Python codes: Monetary system and stress monitoring




오늘 작성한 monetary dashboard 글에 있는 그림을 작성한데 사용한 파이썬 코드 공개. 그림에 사용된 measure와 해석하는 방법은 포스팅에 있는것을 그대로 가져왔다. 코드 및 해설 모두 제미나이의 도움을 받음.
측정 내용 (의도): 이 차트는 금융 시스템 전체의 "연료 탱크"와 같습니다. 시스템 내 총 유동성(현금)이 얼마나 되는지, 그리고 연준(Fed)이 돈을 풀고 있는지(양적 완화, QE) 혹은 거둬들이고 있는지(양적 긴축, QT)를 보여줍니다.
산출 방식 (로직):
파란색 선 (지준금 / 은행 자산): 총 은행 지급준비금(WRESBAL)을 총 은행 자산(TLAACBW027SBOG)으로 나누어 계산합니다. 절대 금액이 아닌 비율(%)을 사용하는 이유는 은행의 전체 규모 대비 "보유 현금"이 얼마나 되는지 보여주기 때문입니다. 이 비율이 높을수록 은행이 매우 안전하고 유동성이 풍부함을 의미합니다.
회색 영역 (ON RRP 사용량): 연준의 익일 역레포(Overnight Reverse Repo, RRPONTSYD) 총 잔액을 나타냅니다. 이는 MMF(머니 마켓 펀드)와 같은 비은행 기관들이 보유한 "초과 현금"이 연준에 예치되어 있는 규모를 보여줍니다.
회색 막대 (유동성 부족 추정 구간): "지급준비금이 부족하다고 여겨지는" 예상 수준인 12-13% 구간을 시각적으로 표시한 기준선입니다.
측정 내용 (의도): 이 차트는 시스템의 "취약성" 또는 "체제(Regime)"를 보여주는 지표입니다. 가장 복잡한 차트이며, "현재 시스템에 현금이 '풍부한' 상태인가, '부족한' 상태인가?"라는 질문에 답을 줍니다.
선이 양(+)의 값일 때: 시스템에 유동성이 '풍부한' 상태입니다. 현금이 너무 많아서 일부를 더하거나 빼도 금리에 영향을 미치지 않습니다. 시스템이 안정적입니다.
선이 음(-)의 값일 때: 시스템에 유동성이 '부족한' 상태입니다. "완충제(버퍼)"가 사라져서, 지급준비금의 작은 변화만으로도 금리가 영향을 받습니다. 시스템이 취약하며 금리 급등에 노출되어 있습니다.
산출 방식 (로직):
먼저, 두 가지 데이터의 주간 변화량을 구합니다.
지급준비금 비율 (그림 1)의 변화량
EFFR 스프레드 (그림 3)의 변화량
그다음, 이 두 변화량 간의 상관관계를 찾기 위해 일정 기간(13주, 26주, 52주) 동안의 롤링 OLS 회귀분석(통계 분석)을 실행합니다.
이 차트는 그 관계의 '기울기'를 보여줍니다. 기울기가 음(-)의 값(선이 마이너스)이라는 것은, 지준금 감소가 스프레드(금리 차) 증가와 상관관계가 있다는 의미입니다. 이는 전형적인 공급-수요 법칙이 작동하는 취약한 "유동성 부족" 체제를 나타냅니다.
측정 내용 (의도): '신용'에 기반한 시장의 "실시간 화재경보기"입니다. 은행들이 (서로 돈 빌려주기를 거부하며) 패닉에 빠졌는지, 혹은 시장이 기업들의 부도(디폴트)를 우려해 패닉에 빠졌는지를 보여줍니다.
산출 방식 (로직):
빨간색 선 (EFFR 스프레드): 은행 간 실제 거래 금리(EFFR)에서 연준의 정책 금리(목표 금리)를 뺀 값입니다. 이 수치가 급등한다는 것은 은행들이 연준의 목표치보다 훨씬 비싼 이자를 받고 서로 돈을 빌려준다는 뜻이며, 이는 은행 시스템의 패닉 신호입니다. (점선은 30일 이동평균으로 "만성적" 스트레스를 보여줍니다.)
보라색 선 (하이일드 스프레드): BAMLH0A0HYM2 데이터를 그대로 사용합니다. 이는 "정크 본드" 스프레드로, 투자자들이 위험한 회사채를 보유하는 대가로 요구하는 추가 금리(가산 금리)입니다. 이 수치가 급등한다는 것은 투자자들이 기업 부도와 실물 경제에 대해 패닉에 빠졌음을 의미합니다.
측정 내용 (의도): "금융 배관"의 문제를 감지하는 경보기입니다. 전체 금융 시스템의 가장 필수적이고 핵심적인 배관 역할을 하는 담보(자산 기반) 레포(Repo) 시장의 스트레스를 보여줍니다.
산출 방식 (로직):
바닥(Floor) & 천장(Ceiling) (파란/회색 선): RRP 금리(바닥)와 할인창 금리(천장)를 주요 정책 금리 기준으로 표시하여 "정책 금리 상하한(Corridor)"을 만듭니다.
녹색/빨간색 선 (시장 금리): 핵심 시장 금리(EFFR 및 SOFR)를 정책 금리 기준으로 표시합니다.
판단 기준: 평상시에는 시장 금리(녹색/빨간색 선)가 이 상하한 안쪽, 주로 바닥(RRP 금리) 근처에서 안정적으로 움직여야 합니다. "위기" (2019년 레포 사태와 같은)는 시장 금리가 이 바닥을 뚫고 격렬하게 급등하는 상황을 말하며, 이는 시스템 핵심 배관에 심각하고 급격한 현금 부족이 발생했음을 보여줍니다. (점선은 이 스트레스의 30일 이동평균을 나타냅니다.)
import pandas_datareader.data as web
import pandas as pd
import datetime
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.patches as mpatches
import yfinance as yf
import statsmodels.api as sm
from statsmodels.regression.rolling import RollingOLS
import warnings
import time
# --- 0. Suppress Warnings ---
warnings.simplefilter(action='ignore', category=FutureWarning)
warnings.simplefilter(action='ignore', category=UserWarning)
# --- 1. Global Settings ---
start_date = datetime.datetime(2018, 1, 1)
end_date = datetime.datetime.now()
rolling_window_weeks = 52
print("--- Running Combined Script ---")
print(f"Data range: {start_date.date()} to {end_date.date()}")
print(f"Rolling elasticity window: {rolling_window_weeks} weeks")
# --- Define Market Event Periods ---
repo_spike_color = 'blue'
repo_spike_2019 = (datetime.datetime(2019, 9, 16), datetime.datetime(2019, 9, 20))
# Colors for dynamic shading
decline_color = 'red'
correction_color = 'orange'
# --- Flags ---
asset_data_fetched = False
dynamic_shading_available = False
try:
# --- 2A. Master Data Fetch (FRED) ---
print("\nFetching all required FRED data...")
daily_series_ids = ['EFFR', 'IORB', 'IOER', 'SOFR', 'RRPONTSYAWARD', 'DPCREDIT',
'RRPONTSYD', 'BAMLH0A0HYM2']
weekly_series_ids = ['WRESBAL', 'TLAACBW027SBOG']
daily_data_raw = web.DataReader(daily_series_ids, 'fred', start_date, end_date)
weekly_data_raw = web.DataReader(weekly_series_ids, 'fred', start_date, end_date)
# --- 2B. ROBUST ASSET FETCH (Yahoo Finance) ---
print("Fetching Asset Price data (S&P 500, Nasdaq, Dow, Russell, BTC)...")
asset_tickers = ['^GSPC', '^IXIC', 'BTC-USD', '^DJI', '^RUT']
asset_prices_full = pd.DataFrame()
try:
asset_data_raw = yf.download(asset_tickers, start=start_date, end=end_date)
if asset_data_raw.empty:
print("\nWARNING: Asset data download was empty. Skipping overlays.")
else:
if 'Adj Close' in asset_data_raw.columns:
asset_prices_full = asset_data_raw['Adj Close']
elif 'Close' in asset_data_raw.columns:
print("\nWARNING: 'Adj Close' not found. Falling back to 'Close' prices.")
asset_prices_full = asset_data_raw['Close']
else:
raise KeyError("No valid price columns found.")
asset_prices_full = pd.DataFrame(asset_prices_full).ffill()
downloaded_tickers = [t for t in asset_tickers if t in asset_prices_full.columns]
if downloaded_tickers:
asset_prices_full = asset_prices_full[downloaded_tickers]
asset_data_fetched = True
print(f"Asset data fetched successfully for: {downloaded_tickers}")
else:
print("\nWARNING: No valid asset ticker data found after processing.")
except Exception as e:
print(f"\nWARNING: Failed to fetch asset price data. Skipping asset overlays. Error: {e}")
time.sleep(1)
# --- 3A. Master Data Processing (FRED) ---
print("Processing FRED data...")
daily_data = daily_data_raw.ffill()
daily_data['POLICY_RATE'] = daily_data['IORB'].fillna(daily_data['IOER'])
daily_data['EFFR_Spread'] = (daily_data['EFFR'] - daily_data['POLICY_RATE']) * 100
daily_data['SOFR_Spread'] = (daily_data['SOFR'] - daily_data['POLICY_RATE']) * 100
daily_data['RRP_Spread'] = (daily_data['RRPONTSYAWARD'] - daily_data['POLICY_RATE']) * 100
daily_data['DW_Spread'] = (daily_data['DPCREDIT'] - daily_data['POLICY_RATE']) * 100
if 'RRPONTSYD' in daily_data.columns:
daily_data['RRPONTSYD_B'] = daily_data['RRPONTSYD'] / 1000
weekly_data_raw['RESERVE_RATIO_PCT'] = (weekly_data_raw['WRESBAL'] / weekly_data_raw['TLAACBW027SBOG']) * 100
# --- 3B. ROBUST Asset Price Processing ---
if asset_data_fetched:
print("Processing and normalizing asset prices...")
asset_norm = pd.DataFrame(index=asset_prices_full.index)
for col in asset_prices_full.columns:
first_valid_price = ...


