Performance Of Various Asset Classes During Fed Policy Cycles¶

Python Imports¶

In [1]:
# Standard Library
import datetime
import io
import os
import random
import sys
import warnings

from datetime import datetime, timedelta
from pathlib import Path

# Data Handling
import numpy as np
import pandas as pd

# Data Visualization
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import seaborn as sns
from matplotlib.ticker import FormatStrFormatter, FuncFormatter, MultipleLocator

# Data Sources
import yfinance as yf
import pandas_datareader.data as web

# Statistical Analysis
import statsmodels.api as sm

# Machine Learning
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

# Suppress warnings
warnings.filterwarnings("ignore")

Add Directories To Path¶

In [2]:
# Add the source subdirectory to the system path to allow import config from settings.py
current_directory = Path(os.getcwd())
website_base_directory = current_directory.parent.parent.parent
src_directory = website_base_directory / "src"
sys.path.append(str(src_directory)) if str(src_directory) not in sys.path else None

# Import settings.py
from settings import config

# Add configured directories from config to path
SOURCE_DIR = config("SOURCE_DIR")
sys.path.append(str(Path(SOURCE_DIR))) if str(Path(SOURCE_DIR)) not in sys.path else None

# Add other configured directories
BASE_DIR = config("BASE_DIR")
CONTENT_DIR = config("CONTENT_DIR")
POSTS_DIR = config("POSTS_DIR")
PAGES_DIR = config("PAGES_DIR")
PUBLIC_DIR = config("PUBLIC_DIR")
SOURCE_DIR = config("SOURCE_DIR")
DATA_DIR = config("DATA_DIR")
DATA_MANUAL_DIR = config("DATA_MANUAL_DIR")

# Print system path
for i, path in enumerate(sys.path):
    print(f"{i}: {path}")
0: /usr/lib/python313.zip
1: /usr/lib/python3.13
2: /usr/lib/python3.13/lib-dynload
3: 
4: /home/jared/python-virtual-envs/general_313/lib/python3.13/site-packages
5: /home/jared/python-virtual-envs/general_313/lib/python3.13/site-packages/setuptools/_vendor
6: /home/jared/Cloud_Storage/Dropbox/Websites/jaredszajkowski.github.io/src

Track Index Dependencies¶

In [3]:
# Create file to track markdown dependencies
dep_file = Path("index_dep.txt")
dep_file.write_text("")
Out[3]:
0

Python Functions¶

In [4]:
from calc_fed_cycle_asset_performance import calc_fed_cycle_asset_performance
from df_info import df_info
from df_info_markdown import df_info_markdown
from export_track_md_deps import export_track_md_deps
from load_data import load_data
from pandas_set_decimal_places import pandas_set_decimal_places
from plot_bar_returns_ffr_change import plot_bar_returns_ffr_change
from plot_timeseries import plot_timeseries
from plot_scatter_regression_ffr_vs_returns import plot_scatter_regression_ffr_vs_returns
from sm_ols_summary_markdown import sm_ols_summary_markdown
from summary_stats import summary_stats
from yf_pull_data import yf_pull_data

Data Overview¶

In [5]:
# Set timeframe
start_date = "2004-11-30"  # GLD inception (month end)
end_date = "2025-10-31"

Acquire & Plot Fed Funds Data¶

In [6]:
# Set decimal places
pandas_set_decimal_places(4)

# Pull Effective Fed Funds Rate from FRED
fedfunds = web.DataReader("FEDFUNDS", "fred", start="1900-01-01", end=datetime.today())
fedfunds["FEDFUNDS"] = fedfunds["FEDFUNDS"] / 100  # Convert to decimal

# Resample to monthly frequency and compute change in rate
fedfunds_monthly = fedfunds.resample("M").last()
fedfunds_monthly = fedfunds_monthly[(fedfunds_monthly.index >= pd.to_datetime(start_date)) & (fedfunds_monthly.index <= pd.to_datetime(end_date))]
fedfunds_monthly["FedFunds_Change"] = fedfunds_monthly["FEDFUNDS"].diff()
In [7]:
df_info(fedfunds_monthly)
The columns, shape, and data types are:
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 252 entries, 2004-11-30 to 2025-10-31
Freq: ME
Data columns (total 2 columns):
 #   Column           Non-Null Count  Dtype  
---  ------           --------------  -----  
 0   FEDFUNDS         252 non-null    float64
 1   FedFunds_Change  251 non-null    float64
dtypes: float64(2)
memory usage: 5.9 KB
None
The first 5 rows are:
FEDFUNDS FedFunds_Change
DATE
2004-11-30 0.0193 NaN
2004-12-31 0.0216 0.0023
2005-01-31 0.0228 0.0012
2005-02-28 0.0250 0.0022
2005-03-31 0.0263 0.0013
The last 5 rows are:
FEDFUNDS FedFunds_Change
DATE
2025-06-30 0.0433 0.0000
2025-07-31 0.0433 0.0000
2025-08-31 0.0433 0.0000
2025-09-30 0.0422 -0.0011
2025-10-31 0.0409 -0.0013
In [8]:
# Copy this <!-- INSERT_01_Fed_Funds_Monthly_Rate_Change_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_Fed_Funds_Monthly_Rate_Change.md", content=df_info_markdown(df=fedfunds_monthly, decimal_places=4))
✅ Exported and tracked: 01_Fed_Funds_Monthly_Rate_Change.md
In [9]:
plot_timeseries(
    price_df=fedfunds_monthly,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["FEDFUNDS"],
    title="Fed Funds Rate",
    x_label="Date",
    x_format="Year",
    y_label="Rate (%)",
    y_format="Percentage",
    y_format_decimal_places=1,
    y_tick_spacing=0.005,
    grid=True,
    legend=False,
    export_plot=True,
    plot_file_name="01_Fed_Funds_Rate",
)
No description has been provided for this image
In [10]:
plot_timeseries(
    price_df=fedfunds_monthly,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["FedFunds_Change"],
    title="Fed Funds Change In Rate",
    x_label="Date",
    x_format="Year",
    y_label="Rate (%)",
    y_format="Percentage",
    y_format_decimal_places=2,
    y_tick_spacing=0.0025,
    grid=True,
    legend=False,
    export_plot=True,
    plot_file_name="01_Fed_Funds_Change_In_Rate",
)
No description has been provided for this image

Define Fed Policy Cycles¶

In [11]:
# # Define manually specified Fed policy cycles
# fed_cycles = [
#     # ("2002-01-01", "2003-07-01"),
#     # ("2003-07-01", "2004-06-01"),
#     # ("2004-06-01", "2006-07-01"),
#     ("2004-11-01", "2006-07-01"),
#     ("2006-07-01", "2007-07-01"),
#     ("2007-07-01", "2008-12-01"),
#     ("2008-12-01", "2015-11-01"),
#     ("2015-11-01", "2019-01-01"),
#     ("2019-01-01", "2019-07-01"),
#     ("2019-07-01", "2020-04-01"),
#     ("2020-04-01", "2022-02-01"),
#     ("2022-02-01", "2023-08-01"),
#     ("2023-08-01", "2024-08-01"),
#     ("2024-08-01", datetime.today().strftime('%Y-%m-%d')),
# ]

# # Optional: assign a name to each cycle
# cycle_labels = [f"Cycle {i+1}" for i in range(len(fed_cycles))]
In [12]:
# Define manually specified Fed policy cycles
fed_cycles = [
    ("2004-11-01", "2006-07-01"),
    ("2006-07-01", "2007-07-01"),
    ("2007-07-01", "2008-12-01"),
    ("2008-12-01", "2015-11-01"),
    ("2015-11-01", "2019-01-01"),
    ("2019-01-01", "2019-07-01"),
    ("2019-07-01", "2020-04-01"),
    ("2020-04-01", "2022-02-01"),
    ("2022-02-01", "2023-08-01"),
    ("2023-08-01", "2024-08-01"),
    ("2024-08-01", datetime.today().strftime('%Y-%m-%d')),
]

# Optional: assign a name to each cycle
cycle_labels = [f"Cycle {i+1}" for i in range(len(fed_cycles))]
In [13]:
# Set decimal places
pandas_set_decimal_places(4)

#  Calc changes by fed cycle defined above
fed_changes = []

for (start, end) in fed_cycles:
    start = pd.to_datetime(start)
    end = pd.to_datetime(end)

    try:
        rate_start = fedfunds.loc[start, "FEDFUNDS"]
    except KeyError:
        rate_start = fedfunds.loc[:start].iloc[-1]["FEDFUNDS"]

    try:
        rate_end = fedfunds.loc[end, "FEDFUNDS"]
    except KeyError:
        rate_end = fedfunds.loc[:end].iloc[-1]["FEDFUNDS"]

    change = rate_end - rate_start
    fed_changes.append(change)

fed_changes_df = pd.DataFrame({
    "Cycle": cycle_labels,
    "FedFunds_Change": fed_changes
})

fed_changes_df
Out[13]:
Cycle FedFunds_Change
0 Cycle 1 0.0331
1 Cycle 2 0.0002
2 Cycle 3 -0.0510
3 Cycle 4 -0.0004
4 Cycle 5 0.0228
5 Cycle 6 0.0000
6 Cycle 7 -0.0235
7 Cycle 8 0.0003
8 Cycle 9 0.0525
9 Cycle 10 0.0000
10 Cycle 11 -0.0124
In [14]:
# Copy this <!-- INSERT_01_Fed_Funds_Cycle_Change_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_Fed_Funds_Cycle_Change.md", content=fed_changes_df.to_markdown(floatfmt=".4f"))
✅ Exported and tracked: 01_Fed_Funds_Cycle_Change.md

Return Performance By Fed Policy Cycle¶

Stocks (SPY)¶

In [15]:
# Set decimal places
pandas_set_decimal_places(2)

yf_pull_data(
    base_directory=DATA_DIR,
    ticker="SPY",
    source="Yahoo_Finance", 
    asset_class="Exchange_Traded_Funds", 
    excel_export=True,
    pickle_export=True,
    output_confirmation=True,
)
YF.download() has changed argument auto_adjust default to True
[*********************100%***********************]  1 of 1 completed

The first and last date of data for SPY is: 
Close High Low Open Volume
Date
1993-01-29 24.31 24.33 24.21 24.33 1003200
Close High Low Open Volume
Date
2025-11-26 679.68 681.70 676.72 677.63 71879600
Yahoo Finance data complete for SPY
--------------------
Out[15]:
Close High Low Open Volume
Date
1993-01-29 24.31 24.33 24.21 24.33 1003200
1993-02-01 24.49 24.49 24.33 24.33 480500
1993-02-02 24.54 24.56 24.42 24.47 201300
1993-02-03 24.80 24.81 24.56 24.57 529400
1993-02-04 24.90 24.95 24.61 24.88 531500
... ... ... ... ... ...
2025-11-20 652.53 675.56 651.89 672.91 165293500
2025-11-21 659.03 664.55 650.85 655.05 123956200
2025-11-24 668.73 670.06 661.59 662.69 80437900
2025-11-25 675.02 676.21 664.48 668.63 81077100
2025-11-26 679.68 681.70 676.72 677.63 71879600

8265 rows × 5 columns

In [16]:
spy = load_data(
    base_directory=DATA_DIR,
    ticker="SPY",
    source="Yahoo_Finance", 
    asset_class="Exchange_Traded_Funds",
    timeframe="Daily",
    file_format="pickle",
)

# Filter SPY to date range
spy = spy[(spy.index >= pd.to_datetime(start_date)) & (spy.index <= pd.to_datetime(end_date))]

# Resample to monthly frequency
spy_monthly = spy.resample("M").last()
spy_monthly["Monthly_Return"] = spy_monthly["Close"].pct_change()
In [17]:
df_info(spy_monthly)
The columns, shape, and data types are:
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 252 entries, 2004-11-30 to 2025-10-31
Freq: ME
Data columns (total 6 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Close           252 non-null    float64
 1   High            252 non-null    float64
 2   Low             252 non-null    float64
 3   Open            252 non-null    float64
 4   Volume          252 non-null    int64  
 5   Monthly_Return  251 non-null    float64
dtypes: float64(5), int64(1)
memory usage: 13.8 KB
None
The first 5 rows are:
Close High Low Open Volume Monthly_Return
Date
2004-11-30 79.83 80.07 79.66 79.90 53685200 NaN
2004-12-31 82.23 82.77 82.19 82.53 28648800 0.03
2005-01-31 80.39 80.45 80.08 80.25 52532700 -0.02
2005-02-28 82.07 82.53 81.67 82.43 69381300 0.02
2005-03-31 80.57 80.91 80.51 80.73 64575400 -0.02
The last 5 rows are:
Close High Low Open Volume Monthly_Return
Date
2025-06-30 616.14 617.51 613.34 615.67 92502500 0.05
2025-07-31 630.33 638.08 629.03 637.69 103385200 0.02
2025-08-31 643.27 646.05 641.36 645.68 74522200 0.02
2025-09-30 666.18 666.65 661.61 662.93 86288000 0.04
2025-10-31 682.06 685.08 679.24 685.04 87164100 0.02
In [18]:
# Copy this <!-- INSERT_02_SPY_Monthly_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_SPY_Monthly.md", content=df_info_markdown(df=spy_monthly, decimal_places=2))
✅ Exported and tracked: 02_SPY_Monthly.md
In [19]:
plot_timeseries(
    price_df=spy,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["Close"],
    title="SPY Close Price",
    x_label="Date",
    x_format="Year",
    y_label="Price ($)",
    y_format="Decimal",
    y_format_decimal_places=0,
    y_tick_spacing=50,
    grid=True,
    legend=False,
    export_plot=True,
    plot_file_name="02_SPY_Price",
)
No description has been provided for this image
In [20]:
spy_cycle_df = calc_fed_cycle_asset_performance(
    fed_cycles=fed_cycles,
    cycle_labels=cycle_labels,
    fed_changes=fed_changes,
    monthly_df=spy_monthly,
)

spy_cycle_df
Out[20]:
Cycle Start End Months CumulativeReturn CumulativeReturnPct AverageMonthlyReturn AverageMonthlyReturnPct AnnualizedReturn AnnualizedReturnPct Volatility FedFundsChange FedFundsChange_bps FFR_AnnualizedChange FFR_AnnualizedChange_bps Label
0 Cycle 1 2004-11-01 2006-07-01 20 0.11 11.32 0.01 0.59 0.07 6.64 0.08 0.03 331.00 0.02 198.60 Cycle 1, 2004-11-01 to 2006-07-01
1 Cycle 2 2006-07-01 2007-07-01 12 0.20 20.36 0.02 1.57 0.20 20.36 0.07 0.00 2.00 0.00 2.00 Cycle 2, 2006-07-01 to 2007-07-01
2 Cycle 3 2007-07-01 2008-12-01 17 -0.39 -38.55 -0.03 -2.67 -0.29 -29.09 0.19 -0.05 -510.00 -0.04 -360.00 Cycle 3, 2007-07-01 to 2008-12-01
3 Cycle 4 2008-12-01 2015-11-01 83 1.67 167.34 0.01 1.28 0.15 15.28 0.15 -0.00 -4.00 -0.00 -0.58 Cycle 4, 2008-12-01 to 2015-11-01
4 Cycle 5 2015-11-01 2019-01-01 38 0.28 28.30 0.01 0.70 0.08 8.19 0.11 0.02 228.00 0.01 72.00 Cycle 5, 2015-11-01 to 2019-01-01
5 Cycle 6 2019-01-01 2019-07-01 6 0.18 18.33 0.03 2.95 0.40 40.01 0.18 0.00 0.00 0.00 0.00 Cycle 6, 2019-01-01 to 2019-07-01
6 Cycle 7 2019-07-01 2020-04-01 9 -0.11 -10.67 -0.01 -1.10 -0.14 -13.96 0.19 -0.02 -235.00 -0.03 -313.33 Cycle 7, 2019-07-01 to 2020-04-01
7 Cycle 8 2020-04-01 2022-02-01 22 0.79 79.13 0.03 2.78 0.37 37.43 0.16 0.00 3.00 0.00 1.64 Cycle 8, 2020-04-01 to 2022-02-01
8 Cycle 9 2022-02-01 2023-08-01 18 0.04 4.18 0.00 0.40 0.03 2.77 0.21 0.05 525.00 0.03 350.00 Cycle 9, 2022-02-01 to 2023-08-01
9 Cycle 10 2023-08-01 2024-08-01 12 0.22 22.00 0.02 1.75 0.22 22.00 0.15 0.00 0.00 0.00 0.00 Cycle 10, 2023-08-01 to 2024-08-01
10 Cycle 11 2024-08-01 2025-11-30 15 0.26 25.72 0.02 1.59 0.20 20.09 0.11 -0.01 -124.00 -0.01 -99.20 Cycle 11, 2024-08-01 to 2025-11-30
In [21]:
# Copy this <!-- INSERT_02_SPY_Cycle_DF_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_SPY_Cycle_DF.md", content=spy_cycle_df.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 02_SPY_Cycle_DF.md
In [22]:
plot_bar_returns_ffr_change(
    cycle_df=spy_cycle_df,
    asset_label="SPY",
    annualized_or_cumulative="Cumulative",
    index_num="02",
)
No description has been provided for this image
In [23]:
plot_bar_returns_ffr_change(
    cycle_df=spy_cycle_df,
    asset_label="SPY",
    annualized_or_cumulative="Annualized",
    index_num="02",
)
No description has been provided for this image
In [24]:
df = spy_cycle_df

####################################
### Don't modify below this line ###
####################################

# Run OLS regression with statsmodels
X = df["FFR_AnnualizedChange_bps"]
y = df["AnnualizedReturnPct"]
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
print(model.summary())
print(f"Intercept: {model.params[0]}, Slope: {model.params[1]}")  # Intercept and slope

# Calc X and Y values for regression line
X_vals = np.linspace(X.min(), X.max(), 100)
Y_vals = model.params[0] + model.params[1] * X_vals
                             OLS Regression Results                            
===============================================================================
Dep. Variable:     AnnualizedReturnPct   R-squared:                       0.184
Model:                             OLS   Adj. R-squared:                  0.093
Method:                  Least Squares   F-statistic:                     2.031
Date:                 Sun, 30 Nov 2025   Prob (F-statistic):              0.188
Time:                         07:50:16   Log-Likelihood:                -47.144
No. Observations:                   11   AIC:                             98.29
Df Residuals:                        9   BIC:                             99.08
Df Model:                            1                                         
Covariance Type:             nonrobust                                         
============================================================================================
                               coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------------------
const                       12.3840      5.875      2.108      0.064      -0.907      25.675
FFR_AnnualizedChange_bps     0.0437      0.031      1.425      0.188      -0.026       0.113
==============================================================================
Omnibus:                        1.011   Durbin-Watson:                   3.089
Prob(Omnibus):                  0.603   Jarque-Bera (JB):                0.652
Skew:                           0.032   Prob(JB):                        0.722
Kurtosis:                       1.809   Cond. No.                         192.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Intercept: 12.383961268378005, Slope: 0.04365440514286283
In [25]:
# Copy this <!-- INSERT_02_SPY_Annualized_Regression_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_SPY_Annualized_Regression.md", content=sm_ols_summary_markdown(result=model, file_path="02_SPY_Annualized_Regression.md"))
✅ Exported and tracked: 02_SPY_Annualized_Regression.md
In [26]:
plot_scatter_regression_ffr_vs_returns(
    cycle_df=spy_cycle_df,
    asset_label="SPY",
    index_num="02",
    x_vals=X_vals,
    y_vals=Y_vals,
    intercept=model.params[0],
    slope=model.params[1],
)
No description has been provided for this image

Bonds (TLT)¶

In [27]:
# Set decimal places
pandas_set_decimal_places(2)

yf_pull_data(
    base_directory=DATA_DIR,
    ticker="TLT",
    source="Yahoo_Finance", 
    asset_class="Exchange_Traded_Funds", 
    excel_export=True,
    pickle_export=True,
    output_confirmation=True,
)
[*********************100%***********************]  1 of 1 completed

The first and last date of data for TLT is: 
Close High Low Open Volume
Date
2002-07-30 36.92 37.09 36.92 37.03 6100
Close High Low Open Volume
Date
2025-11-26 90.64 90.64 89.83 90.15 34099000
Yahoo Finance data complete for TLT
--------------------
Out[27]:
Close High Low Open Volume
Date
2002-07-30 36.92 37.09 36.92 37.03 6100
2002-07-31 37.38 37.50 37.09 37.12 29400
2002-08-01 37.59 37.60 37.38 37.38 25000
2002-08-02 37.98 38.09 37.54 37.67 52800
2002-08-05 38.15 38.24 37.98 38.06 61100
... ... ... ... ... ...
2025-11-20 89.23 89.42 88.99 88.99 32945000
2025-11-21 89.50 89.67 89.20 89.65 41938600
2025-11-24 90.01 90.07 89.78 89.95 30865700
2025-11-25 90.24 90.59 90.13 90.31 36503300
2025-11-26 90.64 90.64 89.83 90.15 34099000

5872 rows × 5 columns

In [28]:
tlt = load_data(
    base_directory=DATA_DIR,
    ticker="TLT",
    source="Yahoo_Finance", 
    asset_class="Exchange_Traded_Funds",
    timeframe="Daily",
    file_format="pickle",
)

# Filter TLT to date range
tlt = tlt[(tlt.index >= pd.to_datetime(start_date)) & (tlt.index <= pd.to_datetime(end_date))]

# Resample to monthly frequency
tlt_monthly = tlt.resample("M").last()
tlt_monthly["Monthly_Return"] = tlt_monthly["Close"].pct_change()
In [29]:
df_info(tlt_monthly)
The columns, shape, and data types are:
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 252 entries, 2004-11-30 to 2025-10-31
Freq: ME
Data columns (total 6 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Close           252 non-null    float64
 1   High            252 non-null    float64
 2   Low             252 non-null    float64
 3   Open            252 non-null    float64
 4   Volume          252 non-null    int64  
 5   Monthly_Return  251 non-null    float64
dtypes: float64(5), int64(1)
memory usage: 13.8 KB
None
The first 5 rows are:
Close High Low Open Volume Monthly_Return
Date
2004-11-30 44.13 44.24 43.97 44.13 1754500 NaN
2004-12-31 45.30 45.35 45.17 45.21 1056400 0.03
2005-01-31 46.92 46.94 46.70 46.72 1313900 0.04
2005-02-28 46.22 46.78 46.16 46.78 2797300 -0.01
2005-03-31 46.01 46.05 45.77 45.95 2410900 -0.00
The last 5 rows are:
Close High Low Open Volume Monthly_Return
Date
2025-06-30 86.64 86.83 86.01 86.26 53695200 0.03
2025-07-31 85.65 86.14 85.57 85.86 49814100 -0.01
2025-08-31 85.66 85.92 85.51 85.82 41686400 0.00
2025-09-30 88.74 89.40 88.58 89.03 38584000 0.04
2025-10-31 89.96 90.33 89.88 90.23 38247300 0.01
In [30]:
# Copy this <!-- INSERT_03_TLT_Monthly_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="03_TLT_Monthly.md", content=df_info_markdown(df=tlt_monthly, decimal_places=2))
✅ Exported and tracked: 03_TLT_Monthly.md
In [31]:
plot_timeseries(
    price_df=tlt,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["Close"],
    title="TLT Close Price",
    x_label="Date",
    x_format="Year",
    y_label="Price ($)",
    y_format="Decimal",
    y_format_decimal_places=0,
    y_tick_spacing=10,
    grid=True,
    legend=False,
    export_plot=True,
    plot_file_name="03_TLT_Price",
)
No description has been provided for this image
In [32]:
tlt_cycle_df = calc_fed_cycle_asset_performance(
    fed_cycles=fed_cycles,
    cycle_labels=cycle_labels,
    fed_changes=fed_changes,
    monthly_df=tlt_monthly,
)

tlt_cycle_df
Out[32]:
Cycle Start End Months CumulativeReturn CumulativeReturnPct AverageMonthlyReturn AverageMonthlyReturnPct AnnualizedReturn AnnualizedReturnPct Volatility FedFundsChange FedFundsChange_bps FFR_AnnualizedChange FFR_AnnualizedChange_bps Label
0 Cycle 1 2004-11-01 2006-07-01 20 0.04 4.23 0.00 0.25 0.03 2.51 0.09 0.03 331.00 0.02 198.60 Cycle 1, 2004-11-01 to 2006-07-01
1 Cycle 2 2006-07-01 2007-07-01 12 0.06 5.76 0.00 0.49 0.06 5.76 0.07 0.00 2.00 0.00 2.00 Cycle 2, 2006-07-01 to 2007-07-01
2 Cycle 3 2007-07-01 2008-12-01 17 0.32 32.42 0.02 1.73 0.22 21.92 0.14 -0.05 -510.00 -0.04 -360.00 Cycle 3, 2007-07-01 to 2008-12-01
3 Cycle 4 2008-12-01 2015-11-01 83 0.46 45.67 0.01 0.55 0.06 5.59 0.15 -0.00 -4.00 -0.00 -0.58 Cycle 4, 2008-12-01 to 2015-11-01
4 Cycle 5 2015-11-01 2019-01-01 38 0.07 7.42 0.00 0.23 0.02 2.29 0.10 0.02 228.00 0.01 72.00 Cycle 5, 2015-11-01 to 2019-01-01
5 Cycle 6 2019-01-01 2019-07-01 6 0.10 10.48 0.02 1.73 0.22 22.05 0.13 0.00 0.00 0.00 0.00 Cycle 6, 2019-01-01 to 2019-07-01
6 Cycle 7 2019-07-01 2020-04-01 9 0.26 26.18 0.03 2.73 0.36 36.34 0.18 -0.02 -235.00 -0.03 -313.33 Cycle 7, 2019-07-01 to 2020-04-01
7 Cycle 8 2020-04-01 2022-02-01 22 -0.11 -11.33 -0.00 -0.50 -0.06 -6.35 0.11 0.00 3.00 0.00 1.64 Cycle 8, 2020-04-01 to 2022-02-01
8 Cycle 9 2022-02-01 2023-08-01 18 -0.27 -26.96 -0.02 -1.62 -0.19 -18.90 0.17 0.05 525.00 0.03 350.00 Cycle 9, 2022-02-01 to 2023-08-01
9 Cycle 10 2023-08-01 2024-08-01 12 -0.02 -1.52 0.00 0.02 -0.02 -1.52 0.20 0.00 0.00 0.00 0.00 Cycle 10, 2023-08-01 to 2024-08-01
10 Cycle 11 2024-08-01 2025-11-30 15 0.00 0.42 0.00 0.08 0.00 0.33 0.11 -0.01 -124.00 -0.01 -99.20 Cycle 11, 2024-08-01 to 2025-11-30
In [33]:
# Copy this <!-- INSERT_03_TLT_Cycle_DF_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="03_TLT_Cycle_DF.md", content=tlt_cycle_df.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 03_TLT_Cycle_DF.md
In [34]:
plot_bar_returns_ffr_change(
    cycle_df=tlt_cycle_df,
    asset_label="TLT",
    annualized_or_cumulative="Cumulative",
    index_num="03",
)
No description has been provided for this image
In [35]:
plot_bar_returns_ffr_change(
    cycle_df=tlt_cycle_df,
    asset_label="TLT",
    annualized_or_cumulative="Annualized",
    index_num="03",
)
No description has been provided for this image
In [36]:
df = tlt_cycle_df

####################################
### Don't modify below this line ###
####################################

# Run OLS regression with statsmodels
X = df["FFR_AnnualizedChange_bps"]
y = df["AnnualizedReturnPct"]
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
print(model.summary())
print(f"Intercept: {model.params[0]}, Slope: {model.params[1]}")  # Intercept and slope

# Calc X and Y values for regression line
X_vals = np.linspace(X.min(), X.max(), 100)
Y_vals = model.params[0] + model.params[1] * X_vals
                             OLS Regression Results                            
===============================================================================
Dep. Variable:     AnnualizedReturnPct   R-squared:                       0.634
Model:                             OLS   Adj. R-squared:                  0.593
Method:                  Least Squares   F-statistic:                     15.56
Date:                 Sun, 30 Nov 2025   Prob (F-statistic):            0.00338
Time:                         07:50:20   Log-Likelihood:                -39.515
No. Observations:                   11   AIC:                             83.03
Df Residuals:                        9   BIC:                             83.83
Df Model:                            1                                         
Covariance Type:             nonrobust                                         
============================================================================================
                               coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------------------
const                        5.5490      2.937      1.890      0.091      -1.094      12.192
FFR_AnnualizedChange_bps    -0.0604      0.015     -3.944      0.003      -0.095      -0.026
==============================================================================
Omnibus:                        0.797   Durbin-Watson:                   1.248
Prob(Omnibus):                  0.671   Jarque-Bera (JB):                0.712
Skew:                           0.441   Prob(JB):                        0.701
Kurtosis:                       2.121   Cond. No.                         192.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Intercept: 5.549033327014294, Slope: -0.06038101504707395
In [37]:
# Copy this <!-- INSERT_03_TLT_Annualized_Regression_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="03_TLT_Annualized_Regression.md", content=sm_ols_summary_markdown(result=model, file_path="03_TLT_Annualized_Regression.md"))
✅ Exported and tracked: 03_TLT_Annualized_Regression.md
In [38]:
plot_scatter_regression_ffr_vs_returns(
    cycle_df=tlt_cycle_df,
    asset_label="TLT",
    index_num="03",
    x_vals=X_vals,
    y_vals=Y_vals,
    intercept=model.params[0],
    slope=model.params[1],
)
No description has been provided for this image

Gold (GLD)¶

In [39]:
# Set decimal places
pandas_set_decimal_places(2)

yf_pull_data(
    base_directory=DATA_DIR,
    ticker="GLD",
    source="Yahoo_Finance", 
    asset_class="Exchange_Traded_Funds", 
    excel_export=True,
    pickle_export=True,
    output_confirmation=True,
)
[*********************100%***********************]  1 of 1 completed

The first and last date of data for GLD is: 
Close High Low Open Volume
Date
2004-11-18 44.38 44.49 44.07 44.43 5992000
Close High Low Open Volume
Date
2025-11-26 383.12 383.78 380.46 381.06 9184100
Yahoo Finance data complete for GLD
--------------------
Out[39]:
Close High Low Open Volume
Date
2004-11-18 44.38 44.49 44.07 44.43 5992000
2004-11-19 44.78 44.92 44.47 44.49 11655300
2004-11-22 44.95 44.97 44.74 44.75 11996000
2004-11-23 44.75 44.92 44.72 44.88 3169200
2004-11-24 45.05 45.05 44.79 44.93 6105100
... ... ... ... ... ...
2025-11-20 374.85 377.69 371.85 375.29 10506500
2025-11-21 374.27 377.23 372.94 374.03 13194600
2025-11-24 380.20 380.28 374.19 374.88 10085300
2025-11-25 380.08 382.52 378.06 380.26 10462800
2025-11-26 383.12 383.78 380.46 381.06 9184100

5290 rows × 5 columns

In [40]:
gld = load_data(
    base_directory=DATA_DIR,
    ticker="GLD",
    source="Yahoo_Finance", 
    asset_class="Exchange_Traded_Funds",
    timeframe="Daily",
    file_format="pickle",
)

# Filter GLD to date range
gld = gld[(gld.index >= pd.to_datetime(start_date)) & (gld.index <= pd.to_datetime(end_date))]

# Resample to monthly frequency
gld_monthly = gld.resample("M").last()
gld_monthly["Monthly_Return"] = gld_monthly["Close"].pct_change()
In [41]:
df_info(gld_monthly)
The columns, shape, and data types are:
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 252 entries, 2004-11-30 to 2025-10-31
Freq: ME
Data columns (total 6 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Close           252 non-null    float64
 1   High            252 non-null    float64
 2   Low             252 non-null    float64
 3   Open            252 non-null    float64
 4   Volume          252 non-null    int64  
 5   Monthly_Return  251 non-null    float64
dtypes: float64(5), int64(1)
memory usage: 13.8 KB
None
The first 5 rows are:
Close High Low Open Volume Monthly_Return
Date
2004-11-30 45.12 45.41 44.82 45.37 3857200 NaN
2004-12-31 43.80 43.94 43.73 43.85 531600 -0.03
2005-01-31 42.22 42.30 41.96 42.21 1692400 -0.04
2005-02-28 43.53 43.74 43.52 43.68 755300 0.03
2005-03-31 42.82 42.87 42.70 42.87 1363200 -0.02
The last 5 rows are:
Close High Low Open Volume Monthly_Return
Date
2025-06-30 304.83 304.92 301.95 302.39 8192100 0.00
2025-07-31 302.96 304.61 302.86 304.59 8981000 -0.01
2025-08-31 318.07 318.09 314.64 314.72 15642600 0.05
2025-09-30 355.47 355.57 350.87 351.13 13312400 0.12
2025-10-31 368.12 370.66 365.50 370.47 11077900 0.04
In [42]:
# Copy this <!-- INSERT_04_GLD_Monthly_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="04_GLD_Monthly.md", content=df_info_markdown(df=gld_monthly, decimal_places=2))
✅ Exported and tracked: 04_GLD_Monthly.md
In [43]:
plot_timeseries(
    price_df=gld,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["Close"],
    title="GLD Close Price",
    x_label="Date",
    x_format="Year",
    y_label="Price ($)",
    y_format="Decimal",
    y_format_decimal_places=0,
    y_tick_spacing=25,
    grid=True,
    legend=False,
    export_plot=True,
    plot_file_name="04_GLD_Price",
)
No description has been provided for this image
In [44]:
gld_cycle_df = calc_fed_cycle_asset_performance(
    fed_cycles=fed_cycles,
    cycle_labels=cycle_labels,
    fed_changes=fed_changes,
    monthly_df=gld_monthly,
)

gld_cycle_df
Out[44]:
Cycle Start End Months CumulativeReturn CumulativeReturnPct AverageMonthlyReturn AverageMonthlyReturnPct AnnualizedReturn AnnualizedReturnPct Volatility FedFundsChange FedFundsChange_bps FFR_AnnualizedChange FFR_AnnualizedChange_bps Label
0 Cycle 1 2004-11-01 2006-07-01 20 0.36 35.70 0.02 1.73 0.20 20.10 0.17 0.03 331.00 0.02 198.60 Cycle 1, 2004-11-01 to 2006-07-01
1 Cycle 2 2006-07-01 2007-07-01 12 0.05 4.96 0.00 0.45 0.05 4.96 0.11 0.00 2.00 0.00 2.00 Cycle 2, 2006-07-01 to 2007-07-01
2 Cycle 3 2007-07-01 2008-12-01 17 0.25 24.96 0.02 1.59 0.17 17.03 0.26 -0.05 -510.00 -0.04 -360.00 Cycle 3, 2007-07-01 to 2008-12-01
3 Cycle 4 2008-12-01 2015-11-01 83 0.36 36.10 0.01 0.51 0.05 4.56 0.18 -0.00 -4.00 -0.00 -0.58 Cycle 4, 2008-12-01 to 2015-11-01
4 Cycle 5 2015-11-01 2019-01-01 38 0.11 10.93 0.00 0.35 0.03 3.33 0.14 0.02 228.00 0.01 72.00 Cycle 5, 2015-11-01 to 2019-01-01
5 Cycle 6 2019-01-01 2019-07-01 6 0.10 9.86 0.02 1.63 0.21 20.68 0.12 0.00 0.00 0.00 0.00 Cycle 6, 2019-01-01 to 2019-07-01
6 Cycle 7 2019-07-01 2020-04-01 9 0.11 11.15 0.01 1.24 0.15 15.13 0.13 -0.02 -235.00 -0.03 -313.33 Cycle 7, 2019-07-01 to 2020-04-01
7 Cycle 8 2020-04-01 2022-02-01 22 0.14 13.54 0.01 0.69 0.07 7.17 0.16 0.00 3.00 0.00 1.64 Cycle 8, 2020-04-01 to 2022-02-01
8 Cycle 9 2022-02-01 2023-08-01 18 0.08 8.48 0.01 0.53 0.06 5.58 0.14 0.05 525.00 0.03 350.00 Cycle 9, 2022-02-01 to 2023-08-01
9 Cycle 10 2023-08-01 2024-08-01 12 0.24 24.24 0.02 1.89 0.24 24.24 0.13 0.00 0.00 0.00 0.00 Cycle 10, 2023-08-01 to 2024-08-01
10 Cycle 11 2024-08-01 2025-11-30 15 0.62 62.49 0.03 3.36 0.47 47.46 0.14 -0.01 -124.00 -0.01 -99.20 Cycle 11, 2024-08-01 to 2025-11-30
In [45]:
# Copy this <!-- INSERT_04_GLD_Cycle_DF_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="04_GLD_Cycle_DF.md", content=gld_cycle_df.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 04_GLD_Cycle_DF.md
In [46]:
plot_bar_returns_ffr_change(
    cycle_df=gld_cycle_df,
    asset_label="GLD",
    annualized_or_cumulative="Cumulative",
    index_num="04",
)
No description has been provided for this image
In [47]:
plot_bar_returns_ffr_change(
    cycle_df=gld_cycle_df,
    asset_label="GLD",
    annualized_or_cumulative="Annualized",
    index_num="04",
)
No description has been provided for this image
In [48]:
df = gld_cycle_df

####################################
### Don't modify below this line ###
####################################

# Run OLS regression with statsmodels
X = df["FFR_AnnualizedChange_bps"]
y = df["AnnualizedReturnPct"]
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
print(model.summary())
print(f"Intercept: {model.params[0]}, Slope: {model.params[1]}")  # Intercept and slope

# Calc X and Y values for regression line
X_vals = np.linspace(X.min(), X.max(), 100)
Y_vals = model.params[0] + model.params[1] * X_vals
                             OLS Regression Results                            
===============================================================================
Dep. Variable:     AnnualizedReturnPct   R-squared:                       0.073
Model:                             OLS   Adj. R-squared:                 -0.030
Method:                  Least Squares   F-statistic:                    0.7118
Date:                 Sun, 30 Nov 2025   Prob (F-statistic):              0.421
Time:                         07:50:25   Log-Likelihood:                -42.895
No. Observations:                   11   AIC:                             89.79
Df Residuals:                        9   BIC:                             90.59
Df Model:                            1                                         
Covariance Type:             nonrobust                                         
============================================================================================
                               coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------------------
const                       15.2394      3.993      3.817      0.004       6.207      24.272
FFR_AnnualizedChange_bps    -0.0176      0.021     -0.844      0.421      -0.065       0.030
==============================================================================
Omnibus:                        8.464   Durbin-Watson:                   0.918
Prob(Omnibus):                  0.015   Jarque-Bera (JB):                3.915
Skew:                           1.356   Prob(JB):                        0.141
Kurtosis:                       4.091   Cond. No.                         192.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Intercept: 15.239380036887518, Slope: -0.017561964787912318
In [49]:
# Copy this <!-- INSERT_04_GLD_Annualized_Regression_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="04_GLD_Annualized_Regression.md", content=sm_ols_summary_markdown(result=model, file_path="04_GLD_Annualized_Regression.md"))
✅ Exported and tracked: 04_GLD_Annualized_Regression.md
In [50]:
plot_scatter_regression_ffr_vs_returns(
    cycle_df=gld_cycle_df,
    asset_label="GLD",
    index_num="04",
    x_vals=X_vals,
    y_vals=Y_vals,
    intercept=model.params[0],
    slope=model.params[1],
)
No description has been provided for this image

Hybrid Portfolio¶

Asset Allocation¶

In [51]:
fed_cycles
Out[51]:
[('2004-11-01', '2006-07-01'),
 ('2006-07-01', '2007-07-01'),
 ('2007-07-01', '2008-12-01'),
 ('2008-12-01', '2015-11-01'),
 ('2015-11-01', '2019-01-01'),
 ('2019-01-01', '2019-07-01'),
 ('2019-07-01', '2020-04-01'),
 ('2020-04-01', '2022-02-01'),
 ('2022-02-01', '2023-08-01'),
 ('2023-08-01', '2024-08-01'),
 ('2024-08-01', '2025-11-30')]
In [52]:
cycle_labels
Out[52]:
['Cycle 1',
 'Cycle 2',
 'Cycle 3',
 'Cycle 4',
 'Cycle 5',
 'Cycle 6',
 'Cycle 7',
 'Cycle 8',
 'Cycle 9',
 'Cycle 10',
 'Cycle 11']
In [53]:
# Calculate cumulative returns and drawdown for SPY
spy_monthly['Cumulative_Return'] = (1 + spy_monthly['Monthly_Return']).cumprod() - 1
spy_monthly['Cumulative_Return_Plus_One'] = 1 + spy_monthly['Cumulative_Return']
spy_monthly['Rolling_Max'] = spy_monthly['Cumulative_Return_Plus_One'].cummax()
spy_monthly['Drawdown'] = spy_monthly['Cumulative_Return_Plus_One'] / spy_monthly['Rolling_Max'] - 1
spy_monthly.drop(columns=['Cumulative_Return_Plus_One', 'Rolling_Max'], inplace=True)

# Calculate cumulative returns and drawdown for TLT
tlt_monthly['Cumulative_Return'] = (1 + tlt_monthly['Monthly_Return']).cumprod() - 1
tlt_monthly['Cumulative_Return_Plus_One'] = 1 + tlt_monthly['Cumulative_Return']
tlt_monthly['Rolling_Max'] = tlt_monthly['Cumulative_Return_Plus_One'].cummax()
tlt_monthly['Drawdown'] = tlt_monthly['Cumulative_Return_Plus_One'] / tlt_monthly['Rolling_Max'] - 1
tlt_monthly.drop(columns=['Cumulative_Return_Plus_One', 'Rolling_Max'], inplace=True)

# Isolate the returns for SPY and TLT
spy_ret = spy_monthly['Monthly_Return']
tlt_ret = tlt_monthly['Monthly_Return']

# Create a blended portfolio based on Fed policy cycles
portfolio = (
    spy_ret[spy_ret.index <= "2007-07-01"]
    .combine_first(tlt_ret[(tlt_ret.index >= "2007-07-01") & (tlt_ret.index <= "2008-12-01")])
    .combine_first(spy_ret[(spy_ret.index > "2008-12-01") & (spy_ret.index <= "2019-07-01")])
    .combine_first(tlt_ret[(tlt_ret.index >= "2019-07-01") & (tlt_ret.index <= "2020-04-01")])
    .combine_first(spy_ret[(spy_ret.index > "2020-04-01") & (spy_ret.index <= "2024-08-01")])
    .combine_first(tlt_ret[tlt_ret.index > "2024-08-01"])
)

# Convert to DataFrame
portfolio_monthly = portfolio.to_frame(name="Portfolio_Monthly_Return")

# Calculate cumulative returns and drawdown for the portfolio
portfolio_monthly['Portfolio_Cumulative_Return'] = (1 + portfolio_monthly['Portfolio_Monthly_Return']).cumprod() - 1
portfolio_monthly['Portfolio_Cumulative_Return_Plus_One'] = 1 + portfolio_monthly['Portfolio_Cumulative_Return']
portfolio_monthly['Portfolio_Rolling_Max'] = portfolio_monthly['Portfolio_Cumulative_Return_Plus_One'].cummax()
portfolio_monthly['Portfolio_Drawdown'] = portfolio_monthly['Portfolio_Cumulative_Return_Plus_One'] / portfolio_monthly['Portfolio_Rolling_Max'] - 1
portfolio_monthly.drop(columns=['Portfolio_Cumulative_Return_Plus_One', 'Portfolio_Rolling_Max'], inplace=True)

# Merge "spy_monthly" and "tlt_monthly" into "portfolio_monthly" to compare cumulative returns
portfolio_monthly = portfolio_monthly.join(
    spy_monthly['Monthly_Return'].rename('SPY_Monthly_Return'),
    how='left'
).join(
    spy_monthly['Cumulative_Return'].rename('SPY_Cumulative_Return'),
    how='left'
).join(
    spy_monthly['Drawdown'].rename('SPY_Drawdown'),
    how='left'
).join(
    tlt_monthly['Monthly_Return'].rename('TLT_Monthly_Return'),
    how='left'
).join(
    tlt_monthly['Cumulative_Return'].rename('TLT_Cumulative_Return'),
    how='left'
).join(
    tlt_monthly['Drawdown'].rename('TLT_Drawdown'),
    how='left'
)
In [54]:
df_info(portfolio_monthly)
The columns, shape, and data types are:
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 252 entries, 2004-11-30 to 2025-10-31
Freq: ME
Data columns (total 9 columns):
 #   Column                       Non-Null Count  Dtype  
---  ------                       --------------  -----  
 0   Portfolio_Monthly_Return     251 non-null    float64
 1   Portfolio_Cumulative_Return  251 non-null    float64
 2   Portfolio_Drawdown           251 non-null    float64
 3   SPY_Monthly_Return           251 non-null    float64
 4   SPY_Cumulative_Return        251 non-null    float64
 5   SPY_Drawdown                 251 non-null    float64
 6   TLT_Monthly_Return           251 non-null    float64
 7   TLT_Cumulative_Return        251 non-null    float64
 8   TLT_Drawdown                 251 non-null    float64
dtypes: float64(9)
memory usage: 19.7 KB
None
The first 5 rows are:
Portfolio_Monthly_Return Portfolio_Cumulative_Return Portfolio_Drawdown SPY_Monthly_Return SPY_Cumulative_Return SPY_Drawdown TLT_Monthly_Return TLT_Cumulative_Return TLT_Drawdown
Date
2004-11-30 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2004-12-31 0.03 0.03 0.00 0.03 0.03 0.00 0.03 0.03 0.00
2005-01-31 -0.02 0.01 -0.02 -0.02 0.01 -0.02 0.04 0.06 0.00
2005-02-28 0.02 0.03 -0.00 0.02 0.03 -0.00 -0.01 0.05 -0.01
2005-03-31 -0.02 0.01 -0.02 -0.02 0.01 -0.02 -0.00 0.04 -0.02
The last 5 rows are:
Portfolio_Monthly_Return Portfolio_Cumulative_Return Portfolio_Drawdown SPY_Monthly_Return SPY_Cumulative_Return SPY_Drawdown TLT_Monthly_Return TLT_Cumulative_Return TLT_Drawdown
Date
2025-06-30 0.03 19.00 -0.07 0.05 6.72 0.00 0.03 0.96 -0.41
2025-07-31 -0.01 18.78 -0.08 0.02 6.90 0.00 -0.01 0.94 -0.41
2025-08-31 0.00 18.78 -0.08 0.02 7.06 0.00 0.00 0.94 -0.41
2025-09-30 0.04 19.49 -0.05 0.04 7.34 0.00 0.04 1.01 -0.39
2025-10-31 0.01 19.77 -0.04 0.02 7.54 0.00 0.01 1.04 -0.39
In [55]:
# Copy this <!-- INSERT_05_Portfolio_DF_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="05_Portfolio_DF.md", content=df_info_markdown(df=portfolio_monthly, decimal_places=3))
✅ Exported and tracked: 05_Portfolio_DF.md

Performance Statistics¶

In [56]:
plot_timeseries(
    price_df=portfolio_monthly,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["Portfolio_Monthly_Return", "SPY_Monthly_Return", "TLT_Monthly_Return"],
    title="Portfolio, SPY, and TLT Monthly Returns",
    x_label="Date",
    x_format="Year",
    y_label="Return",
    y_format="Decimal",
    y_format_decimal_places=2,
    y_tick_spacing=0.02,
    grid=True,
    legend=True,
    export_plot=True,
    plot_file_name="05_Monthly_Returns",
)
No description has been provided for this image
In [57]:
plot_timeseries(
    price_df=portfolio_monthly,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["Portfolio_Cumulative_Return", "SPY_Cumulative_Return", "TLT_Cumulative_Return"],
    title="Portfolio, SPY, and TLT Cumulative Returns",
    x_label="Date",
    x_format="Year",
    y_label="Cumulative Return",
    y_format="Decimal",
    y_format_decimal_places=0,
    y_tick_spacing=2,
    grid=True,
    legend=True,
    export_plot=True,
    plot_file_name="05_Cumulative_Returns",
)
No description has been provided for this image
In [58]:
plot_timeseries(
    price_df=portfolio_monthly,
    plot_start_date=start_date,
    plot_end_date=end_date,
    plot_columns=["Portfolio_Drawdown", "SPY_Drawdown", "TLT_Drawdown"],
    title="Portfolio, SPY, and TLT Drawdowns",
    x_label="Date",
    x_format="Year",
    y_label="Drawdown",
    y_format="Decimal",
    y_format_decimal_places=2,
    y_tick_spacing=0.05,
    grid=True,
    legend=True,
    export_plot=True,
    plot_file_name="05_Drawdowns",
)
No description has been provided for this image
In [59]:
port_sum_stats = summary_stats(
    fund_list=["Portfolio", "SPY", "TLT"],
    df=portfolio_monthly[["Portfolio_Monthly_Return"]],
    period="Monthly",
    use_calendar_days=False,
    excel_export=False,
    pickle_export=False,
    output_confirmation=False,
)

spy_sum_stats = summary_stats(
    fund_list=["Portfolio", "SPY", "TLT"],
    df=portfolio_monthly[["SPY_Monthly_Return"]],
    period="Monthly",
    use_calendar_days=False,
    excel_export=False,
    pickle_export=False,
    output_confirmation=False,
)

tlt_sum_stats = summary_stats(
    fund_list=["Portfolio", "SPY", "TLT"],
    df=portfolio_monthly[["TLT_Monthly_Return"]],
    period="Monthly",
    use_calendar_days=False,
    excel_export=False,
    pickle_export=False,
    output_confirmation=False,
)

sum_stats = port_sum_stats.combine_first(spy_sum_stats).combine_first(tlt_sum_stats)
In [60]:
# Copy this <!-- INSERT_05_Portfolio_Stats_DF_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="05_Portfolio_Stats_DF.md", content=sum_stats.to_markdown(floatfmt=".3f"))
✅ Exported and tracked: 05_Portfolio_Stats_DF.md