Cleaning Summary: Covered Interest Parity (CIP) Arbitrage Spreads#

Source: Du, W., & Schreger, J. (2023). Covered Interest Parity Deviations: Macro Risks and Market Frictions. Harvard Business School. Link to paper

Data Collection and Processing Pipeline#

This analysis examines Covered Interest Parity (CIP) deviations using a two-stage data processing pipeline:

  1. Data Retrieval (pull_bbg_foreign_exchange.py): Fetches raw foreign exchange data from Bloomberg Terminal, including spot exchange rates, 3-month forward points, and Overnight Index Swap (OIS) rates for eight G10 currencies versus USD.

  2. CIP Calculation (calc_cip.py): Processes the raw data to compute CIP arbitrage spreads, applying standardized transformations and outlier detection methods.

import sys
from pathlib import Path

sys.path.insert(0, "../../src")
sys.path.insert(0, "./src")

import calc_cip
import cip_analysis
import pandas as pd

from settings import config

DATA_DIR = Path(config("DATA_DIR")) / "cip"

Data Sources and Coverage#

Our analysis examines Covered Interest Parity (CIP) deviations across eight G10 currencies against the USD, using data from 1999 onwards sourced through Bloomberg Terminal. The dataset includes:

  • Spot exchange rates: Current market rates for currency pairs

  • 3-month forward points: Market-quoted forward premiums/discounts

  • Overnight Index Swap (OIS) rates: Risk-free benchmark rates for each currency

Currency Coverage: AUD, CAD, CHF, EUR, GBP, JPY, NZD, and SEK versus USD

Data Standardization: The analysis accounts for different market quotation conventions:

  • Forward points are scaled appropriately (per 10,000 for most currencies, per 100 for JPY)

  • Currencies conventionally quoted as USD-per-foreign-currency (EUR, GBP, AUD, NZD) are converted to reciprocal rates for consistency

  • OIS rates serve as our risk-free benchmark to align with other arbitrage spread studies

spot_rates = pd.read_parquet(DATA_DIR / "fx_spot_rates.parquet")
fwd_pts = pd.read_parquet(DATA_DIR / "fx_forward_points.parquet")
int_rates = pd.read_parquet(DATA_DIR / "fx_interest_rates.parquet")
cip_table = calc_cip.calculate_cip(data_dir=DATA_DIR)
cip_table.tail()
AUD CAD CHF EUR GBP JPY NZD SEK
index
2025-02-24 0.292534 14.172087 8.982214 NaN 1.935113 22.847121 2.971642 8.420659
2025-02-25 0.172848 13.621653 4.624919 NaN 1.813109 20.249373 4.234551 6.477765
2025-02-26 1.577240 12.271818 14.994926 NaN 2.266906 28.436180 5.401439 12.681800
2025-02-27 1.664576 15.707525 19.578262 NaN 3.282794 32.314552 2.293667 15.033099
2025-02-28 1.434554 17.483297 19.764014 NaN 3.331081 33.042709 2.293880 14.332039

Data Processing Steps#

The raw Bloomberg data undergoes several standardization steps in pull_bbg_foreign_exchange.py:

  1. Forward Rate Calculation: Forward points are converted to actual forward rates by:

    • Scaling forward points (รท10,000 for most currencies, รท100 for JPY)

    • Adding scaled points to corresponding spot rates

  2. Currency Convention Standardization: To ensure consistent USD-per-foreign-currency quotation:

    • EUR, GBP, AUD, and NZD rates are converted to reciprocals (1.0 / original rate)

    • Applied to both spot and forward rates for these currencies

  3. Data Merging: Spot rates, forward rates, and OIS rates are merged on date indices for comprehensive analysis

CIP Spread Calculation#

Using the prepared data, we calculate CIP arbitrage spreads using the log-linearized formula.

print(f"Max CIP Deviation: {cip_table.max().idxmax()}")
Max CIP Deviation: JPY
cip_table_replicate = calc_cip.calculate_cip(end_date="2020-01-01", data_dir=DATA_DIR)
cip_table_replicate.tail()
AUD CAD CHF EUR GBP JPY NZD SEK
index
2019-12-26 -2.266847 45.379212 49.482634 NaN 33.275712 63.450330 -7.651565 NaN
2019-12-27 -0.501872 42.747994 41.093553 35.335797 21.219397 44.886575 -7.084184 NaN
2019-12-30 -0.608663 36.439406 28.240115 26.613417 13.562790 43.658422 -7.111196 NaN
2019-12-31 1.051805 29.428635 29.884460 27.209910 12.931200 45.631356 -4.563916 NaN
2020-01-01 1.057455 29.441495 29.016847 NaN 12.657879 45.605954 -5.469134 NaN
print(f"Max CIP Deviation: {cip_table_replicate.max().idxmax()}")
Max CIP Deviation: JPY
calc_cip.plot_cip_from_data(
    cip_table_replicate, end_date="2020-01-01", output_suffix="replicate"
)
../_images/ba571581a827dc93e1545c1c4132ff036a9788d2cb9fa71f3fb632f6a61b6497.png

CIP Calculation and Data Cleaning Process#

The CIP spread calculation in calc_cip.py follows these steps:

  1. CIP Formula Application:

    • Uses log-linearized CIP formula: CIP = 10,000 ร— [domestic_rate - (ln(F) - ln(S)) ร— (360/90) - foreign_rate]

    • Domestic rate: Currency-specific OIS rate

    • Foreign rate: USD OIS rate

    • Forward premium: Annualized using 360/90 day convention

  2. Outlier Detection and Cleaning:

    • Implements 45-day rolling window outlier detection

    • Calculates rolling median and mean absolute deviation (MAD)

    • Removes extreme values exceeding 10ร— rolling MAD threshold

    • Replaces outliers with NaN to preserve time series structure

  3. Output Formatting: Final spreads are expressed in basis points with shortened currency labels for analysis

Extended Analysis: 2025 Dataset#

We extend our analysis to include more recent data through 2025 to examine contemporary CIP patterns.

cip_table_2025 = calc_cip.calculate_cip(end_date="2025-01-01", data_dir=DATA_DIR)
cip_table_2025.tail()
AUD CAD CHF EUR GBP JPY NZD SEK
index
2024-12-26 -0.766333 23.128393 24.550246 NaN 5.442398 33.221177 11.513906 15.338563
2024-12-27 -0.479475 22.250737 19.704683 NaN 4.505007 25.733427 12.095516 3.560293
2024-12-30 0.007007 19.622446 12.229501 NaN 3.395406 22.690483 11.731758 8.169759
2024-12-31 1.955078 17.960460 16.511929 NaN 3.207715 22.661975 14.076388 8.532510
2025-01-01 0.911087 18.164521 16.966331 NaN 4.023649 22.194161 13.433583 NaN
calc_cip.plot_cip_from_data(cip_table_2025, end_date="2025-01-01", output_suffix="2025")
../_images/1f8610278b870aa438512404f3ef804df799dc6f52a8a473c05a95882a7edd79.png

Summary Statistics and Cross-Currency Analysis#

The following section provides comprehensive statistical analysis of CIP deviations across currencies and time periods.

cip_table_2025.describe()
AUD CAD CHF EUR GBP JPY NZD SEK
count 5970.000000 5660.000000 3955.000000 5216.000000 5988.000000 5720.000000 5761.000000 4927.000000
mean 1.902974 15.416702 34.407832 26.246745 14.440782 37.639820 0.036919 29.500217
std 24.277389 20.871205 25.960965 28.142437 19.107388 31.686964 21.042555 32.372212
min -58.535205 -11.137506 -9.265949 -22.949140 -11.714736 -31.232456 -65.252431 -75.020107
25% -9.055422 3.909675 14.099077 4.896297 5.384825 17.565810 -11.572965 11.210014
50% 1.197543 10.259452 30.458430 21.257006 8.617490 32.102677 -5.542426 22.094620
75% 8.380603 21.575389 48.928764 38.609088 18.266183 49.271931 9.521876 37.419639
max 339.688767 313.189228 181.363535 370.943999 287.634939 439.593674 257.471179 374.543530
# Create a version with the column names that cip_analysis expects
cip_table_2025_for_analysis = cip_table_2025.copy()
cip_table_2025_for_analysis.columns = [
    f"CIP_{col}_ln" for col in cip_table_2025_for_analysis.columns
]
stats = cip_analysis.compute_cip_statistics(cip_table_2025_for_analysis)
cip_analysis.display_cip_summary(stats)
================================================================================
OVERALL CIP STATISTICS (in basis points)
================================================================================
       CIP_AUD_ln  CIP_CAD_ln  CIP_CHF_ln  CIP_EUR_ln  CIP_GBP_ln  CIP_JPY_ln  \
count     5970.00     5660.00     3955.00     5216.00     5988.00     5720.00   
mean         1.90       15.42       34.41       26.25       14.44       37.64   
std         24.28       20.87       25.96       28.14       19.11       31.69   
min        -58.54      -11.14       -9.27      -22.95      -11.71      -31.23   
25%         -9.06        3.91       14.10        4.90        5.38       17.57   
50%          1.20       10.26       30.46       21.26        8.62       32.10   
75%          8.38       21.58       48.93       38.61       18.27       49.27   
max        339.69      313.19      181.36      370.94      287.63      439.59   

       CIP_NZD_ln  CIP_SEK_ln  
count     5761.00     4927.00  
mean         0.04       29.50  
std         21.04       32.37  
min        -65.25      -75.02  
25%        -11.57       11.21  
50%         -5.54       22.09  
75%          9.52       37.42  
max        257.47      374.54  
cip_analysis.display_cip_corr(stats)
================================================================================
CORRELATION MATRIX
================================================================================
            CIP_AUD_ln  CIP_CAD_ln  CIP_CHF_ln  CIP_EUR_ln  CIP_GBP_ln  \
CIP_AUD_ln       1.000       0.714      -0.085       0.393       0.600   
CIP_CAD_ln       0.714       1.000       0.505       0.760       0.830   
CIP_CHF_ln      -0.085       0.505       1.000       0.839       0.730   
CIP_EUR_ln       0.393       0.760       0.839       1.000       0.893   
CIP_GBP_ln       0.600       0.830       0.730       0.893       1.000   
CIP_JPY_ln       0.405       0.767       0.827       0.939       0.849   
CIP_NZD_ln       0.806       0.716      -0.003       0.446       0.529   
CIP_SEK_ln       0.622       0.851       0.766       0.847       0.894   

            CIP_JPY_ln  CIP_NZD_ln  CIP_SEK_ln  
CIP_AUD_ln       0.405       0.806       0.622  
CIP_CAD_ln       0.767       0.716       0.851  
CIP_CHF_ln       0.827      -0.003       0.766  
CIP_EUR_ln       0.939       0.446       0.847  
CIP_GBP_ln       0.849       0.529       0.894  
CIP_JPY_ln       1.000       0.430       0.828  
CIP_NZD_ln       0.430       1.000       0.598  
CIP_SEK_ln       0.828       0.598       1.000  
cip_analysis.display_cip_max_min(stats)
================================================================================
EXTREME CIP DEVIATIONS
================================================================================
Most Positive CIP Deviation: CIP_JPY_ln (37.64 bps)
Most Negative CIP Deviation: CIP_NZD_ln (0.04 bps)