In [1]:
# @title
# --- INSTALL & IMPORTS ---

!pip install -q yfinance matplotlib seaborn

import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm

sns.set(style="whitegrid")

# --- CONFIGURATION VARIABLES ---

# Analysis horizon
START_DATE = "2013-08-01"
END_DATE   = "2025-07-01"

# CIP horizon: 1-month = 1/12, 3-month = 0.25
T_CIP = 1/12

# Smoothing window (in months)
SMOOTH_WINDOW = 3

# Toggle: simulate forward (True) or use real forward data (False if you add it later)
USE_SIMULATED_FORWARD = True

# Simulation noise for forward pricing (market micro deviations)
NOISE_STD = 0.002

print("Imports loaded and configuration initialized.")
Imports loaded and configuration initialized.
In [3]:
# --- LOAD INTEREST RATE SERIES (US & AU) ---

from google.colab import files

print("Please upload: DGS3MO.csv (US 3M rate) and IR3TIB01AUQ156N.csv (AU 3M rate)")
uploaded = files.upload()
print("Uploaded files:", uploaded.keys())

def load_rate_series(path, col_name):
    """Load a CSV FRED-style rate series and return tidy monthly-level data."""
    df = pd.read_csv(path)
    df.columns = ["DATE", col_name]
    df["DATE"] = pd.to_datetime(df["DATE"])
    df.set_index("DATE", inplace=True)
    df = df.sort_index()
    return df

# Load US & AU rates
r_us = load_rate_series("DGS3MO.csv", "r_us")          # US 3M T-bill
r_au = load_rate_series("IR3TIB01AUQ156N.csv", "r_au") # AU 3M interbank rate

# --- Merge + Monthly alignment ---
rates = pd.concat([r_us, r_au], axis=1)

# Convert to monthly frequency (end of month)
rates = rates.resample("M").last()

# Forward fill missing values (common in FRED monthly data)
rates = rates.ffill()

# DROP any rows where BOTH rates are missing (rare)
rates = rates.dropna(how="all")

print("\nCleaned & merged rate dataset:")
display(rates.head(5))
display(rates.tail(5))

print("\nRemaining NaNs per column:")
print(rates.isna().sum())

print(f"\nFinal dataset shape: {rates.shape}")
Please upload: DGS3MO.csv (US 3M rate) and IR3TIB01AUQ156N.csv (AU 3M rate)
Upload widget is only available when the cell has been executed in the current browser session. Please rerun this cell to enable.
Saving DGS3MO.csv to DGS3MO.csv
Saving IR3TIB01AUQ156N.csv to IR3TIB01AUQ156N.csv
Uploaded files: dict_keys(['DGS3MO.csv', 'IR3TIB01AUQ156N.csv'])

Cleaned & merged rate dataset:
/tmp/ipython-input-4183513618.py:26: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.
  rates = rates.resample("M").last()
r_us r_au
DATE
2013-01-31 NaN 2.996667
2013-02-28 NaN 2.996667
2013-03-31 NaN 2.996667
2013-04-30 NaN 2.880000
2013-05-31 NaN 2.880000
r_us r_au
DATE
2025-03-31 4.32 4.206667
2025-04-30 4.31 3.830000
2025-05-31 4.36 3.830000
2025-06-30 4.41 3.830000
2025-07-31 4.40 3.623333
Remaining NaNs per column:
r_us    10
r_au     0
dtype: int64

Final dataset shape: (151, 2)
In [5]:
# --- LOAD AUD/USD SPOT RATE (MONTHLY) ---

# Download daily AUD/USD spot from Yahoo Finance
spot_raw = yf.download("AUDUSD=X", start=START_DATE, end=END_DATE, progress=False)

# Validate download
if spot_raw.empty:
    raise ValueError("Error: AUD/USD data not downloaded. Check ticker or internet connection.")

# Extract adjusted close / close
spot = spot_raw["Close"].copy()
spot.name = "AUDUSD"

# Convert index to datetime and resample to end-of-month average
spot.index = pd.to_datetime(spot.index)
spot = spot.resample("M").mean()

print("Monthly AUD/USD spot rate (average of daily data):")
display(spot.head())
display(spot.tail())

print(f"\nTotal monthly observations: {spot.shape[0]}")
Monthly AUD/USD spot rate (average of daily data):
/tmp/ipython-input-4216977819.py:4: FutureWarning: YF.download() has changed argument auto_adjust default to True
  spot_raw = yf.download("AUDUSD=X", start=START_DATE, end=END_DATE, progress=False)
/tmp/ipython-input-4216977819.py:16: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.
  spot = spot.resample("M").mean()
Ticker AUDUSD=X
Date
2013-08-31 0.903258
2013-09-30 0.927581
2013-10-31 0.951494
2013-11-30 0.933456
2013-12-31 0.898514
Ticker AUDUSD=X
Date
2025-02-28 0.630119
2025-03-31 0.630299
2025-04-30 0.627581
2025-05-31 0.643528
2025-06-30 0.650010
Total monthly observations: 143
In [6]:
# --- MERGE SPOT + INTEREST RATES INTO MASTER DATASET ---

# Align spot & rates and drop months with missing values in any series
df = pd.concat([spot, rates], axis=1)

# Rename columns clearly
df.columns = ["AUDUSD", "r_us", "r_au"]

# Drop rows where *any* of the three are missing
df = df.dropna(how="any")

print("Merged monthly dataset: AUDUSD + US 3M + AU 3M")
display(df.head())
display(df.tail())

print(f"\nFinal dataset shape: {df.shape}")
print("\nRemaining NaNs per column:")
print(df.isna().sum())
Merged monthly dataset: AUDUSD + US 3M + AU 3M
AUDUSD r_us r_au
2013-11-30 0.933456 0.06 2.586667
2013-12-31 0.898514 0.07 2.586667
2014-01-31 0.886307 0.02 2.633333
2014-02-28 0.896497 0.05 2.633333
2014-03-31 0.906614 0.05 2.633333
AUDUSD r_us r_au
2025-02-28 0.630119 4.32 4.206667
2025-03-31 0.630299 4.32 4.206667
2025-04-30 0.627581 4.31 3.830000
2025-05-31 0.643528 4.36 3.830000
2025-06-30 0.650010 4.41 3.830000
Final dataset shape: (140, 3)

Remaining NaNs per column:
AUDUSD    0
r_us      0
r_au      0
dtype: int64
In [10]:
# --- DESCRIPTIVE PLOTS: AUD/USD + INTEREST RATES ---

import matplotlib.pyplot as plt
import seaborn as sns

sns.set(style="whitegrid", context="talk")

# --- Combined plot: Spot rate + interest rates ---
fig, ax1 = plt.subplots(figsize=(11,6))

# Plot AUD/USD (left y-axis)
ax1.plot(df.index, df["AUDUSD"], color="darkgreen", lw=2, label="AUD/USD Spot")
ax1.set_ylabel("AUD/USD Exchange Rate", color="darkgreen")
ax1.tick_params(axis="y", labelcolor="darkgreen")

# Add secondary axis for interest rates
ax2 = ax1.twinx()
ax2.plot(df.index, df["r_us"], color="darkred", lw=2, label="US 3M Rate")
ax2.plot(df.index, df["r_au"], color="darkblue", lw=2, label="Australia 3M Rate")
ax2.set_ylabel("Interest Rate (%)", color="black")

# Title & legends
plt.title("AUD/USD vs US & Australian 3-Month Interest Rates", pad=12)

# Combine legends from both axes
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
fig.legend(lines1 + lines2, labels1 + labels2, loc="upper right", bbox_to_anchor=(0.92, 0.92))

plt.tight_layout()
plt.show()


# --- Rate differential plot ---
plt.figure(figsize=(11,4))
plt.plot(df.index, df["r_us"] - df["r_au"], color="purple", lw=2)
plt.axhline(0, color="black", linestyle="--", lw=1)
plt.title("Interest Rate Differential (US 3M – Australia 3M)")
plt.ylabel("Spread (%)")
plt.xlabel("Date")
plt.tight_layout()
plt.show()
No description has been provided for this image
No description has been provided for this image
In [13]:
# ============================================================
# CLEAN & FORMATTED CIP PLOTS (No Overlaps, Proper Spacing)
# ============================================================

import matplotlib.dates as mdates

# --- 1. Scatter + Regression Line (Clean & Spaced) ---
plt.figure(figsize=(9,6))
sns.regplot(
    x="rate_diff", y="forward_premium", data=reg,
    scatter_kws={"alpha":0.6, "color": "steelblue", "s":55},
    line_kws={"color":"darkred", "lw":2}
)

plt.title("CIP Regression: Forward Premium vs Interest Rate Differential", pad=12)
plt.xlabel("Interest Rate Differential (US - AU, 1M)")
plt.ylabel("Forward Premium ((F - S) / S)")

plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# --- Regression ---
reg = cip[["forward_premium", "rate_diff"]].dropna()
X = sm.add_constant(reg["rate_diff"])
y = reg["forward_premium"]

model = sm.OLS(y, X).fit()

# --- OLS Regression Output ---
print(model.summary())

# --- Predictions & Residuals ---
reg["predicted"] = model.fittedvalues
reg["residuals"] = model.resid

# --- 2. Predicted vs Actual (With Proper Date Formatting) ---
plt.figure(figsize=(12,5))

plt.plot(
    reg.index, reg["forward_premium"],
    label="Actual (Simulated)", color="black", lw=1.4
)
plt.plot(
    reg.index, reg["predicted"],
    label="Predicted (OLS)", color="darkred",
    linestyle="--", lw=2
)

plt.title("Predicted vs Actual Forward Premium", pad=12)
plt.ylabel("Forward Premium")
plt.xlabel("Date")
plt.legend()

# Format dates so they don't overlap
plt.gca().xaxis.set_major_locator(mdates.YearLocator(1))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
plt.xticks(rotation=45)

plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()


# --- 3. Residuals Plot (Clean, Centered, Non-Overlapping) ---
plt.figure(figsize=(12,4))

plt.plot(reg.index, reg["residuals"], lw=1.4, color="purple")
plt.axhline(0, color="black", linestyle="--", lw=1)

plt.title("Regression Residuals Over Time", pad=12)
plt.ylabel("Residual")
plt.xlabel("Date")

# Format dates
plt.gca().xaxis.set_major_locator(mdates.YearLocator(1))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
plt.xticks(rotation=45)

plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
No description has been provided for this image
                            OLS Regression Results                            
==============================================================================
Dep. Variable:        forward_premium   R-squared:                       0.308
Model:                            OLS   Adj. R-squared:                  0.303
Method:                 Least Squares   F-statistic:                     61.51
Date:                Fri, 21 Nov 2025   Prob (F-statistic):           1.08e-12
Time:                        01:00:10   Log-Likelihood:                 680.81
No. Observations:                 140   AIC:                            -1358.
Df Residuals:                     138   BIC:                            -1352.
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         -0.0001      0.000     -0.691      0.491      -0.000       0.000
rate_diff      1.1726      0.150      7.843      0.000       0.877       1.468
==============================================================================
Omnibus:                        0.099   Durbin-Watson:                   2.145
Prob(Omnibus):                  0.952   Jarque-Bera (JB):                0.109
Skew:                           0.059   Prob(JB):                        0.947
Kurtosis:                       2.931   Cond. No.                         939.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
No description has been provided for this image
No description has been provided for this image
In [9]:
# --- CIP-BASED EXCHANGE RATE FORECAST (1-MONTH AHEAD) ---

# Use the last available values
last_row = cip.iloc[-1]

S_t      = last_row["S"]
r_us_t   = last_row["r_us_smooth"]
r_au_t   = last_row["r_au_smooth"]

# CIP theoretical forward (forecast)
F_CIP = S_t * (1 + r_us_t * T) / (1 + r_au_t * T)

# Regression-adjusted forward (optional, more realistic)
alpha = model.params["const"]
beta  = model.params["rate_diff"]
rate_diff_t = (r_us_t - r_au_t) * T
F_reg = S_t * (1 + alpha + beta * rate_diff_t)

print("=== 1-Month Ahead Forecast (CIP) ===")
print(f"Last spot rate (S_t):       {S_t:.4f}")
print(f"CIP-implied forward:        {F_CIP:.4f}")
print(f"Regression-adjusted forward:{F_reg:.4f}")
print(f"Implied appreciation:       {(F_CIP/S_t - 1)*100:.3f}%")
=== 1-Month Ahead Forecast (CIP) ===
Last spot rate (S_t):       0.6404
CIP-implied forward:        0.6407
Regression-adjusted forward:0.6406
Implied appreciation:       0.044%
In [ ]: