Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""FFIEC Bank Financial Data Models."""

import pandas as pd
from datetime import date as dateType
from io import StringIO
from typing import Optional

from pydantic import Field
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_federal_reserve.models.rssd_map import resolve_rssd
from curl_cffi import requests as curl_requests


class FfiecRiskQueryParams(QueryParams):
"""Query parameters for FFIEC FR Y-15 Report."""

rssd_id: str = Field(
description="The Federal Reserve RSSD ID of the institution (e.g., '1039502' for JPMorgan)."
)
date: Optional[dateType] = Field(
default=None,
description="The specific report date (YYYY-MM-DD). Defaults to the most recent filing."
)


class FfiecRiskData(Data):
"""Data schema for FFIEC FR Y-15 Systemic Risk Report."""

rssd_id: str = Field(description="The Federal Reserve RSSD ID.")
report_date: dateType = Field(description="The date of the report.")
total_assets: Optional[float] = Field(
default=None,
description="Total consolidated assets (RISK2170), in USD thousands."
)


class FfiecRiskFetcher(
Fetcher[
FfiecRiskQueryParams,
list[FfiecRiskData],
]
):
"""Fetcher for the FFIEC FR Y-15 Systemic Risk Report."""

@staticmethod
def extract_data(query: FfiecRiskQueryParams, credentials: dict, **kwargs) -> pd.DataFrame:
"""Downloads the FR Y-15 CSV from the FFIEC NIC web portal."""

rssd_id = resolve_rssd(query.rssd_id)
target_date = query.date.strftime("%Y%m%d") if query.date else "20241231"

url = f"https://www.ffiec.gov/npw/FinancialReport/ReturnFinancialReportCSV?rpt=FRY15&id={rssd_id}&dt={target_date}"

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Referer": f"https://www.ffiec.gov/npw/Institution/Profile/{rssd_id}",
"Connection": "keep-alive"
}

try:
session = curl_requests.Session()

# Step 1: visit the profile page to establish a valid session cookie
session.get(
f"https://www.ffiec.gov/npw/Institution/Profile/{rssd_id}",
headers=headers,
impersonate="chrome124"
)

# Step 2: fetch the CSV with the active session
response = session.get(url, headers=headers, impersonate="chrome124")
response.raise_for_status()

return pd.read_csv(StringIO(response.text))

except Exception as e:
raise EmptyDataError(f"Could not retrieve FFIEC data for RSSD {rssd_id}. Error: {e}")

@staticmethod
def transform_data(query: FfiecRiskQueryParams, data: pd.DataFrame, **kwargs) -> list[FfiecRiskData]:
"""Pivots the tall-format FFIEC CSV into a validated OpenBB model."""

if data.empty:
raise EmptyDataError("The FFIEC returned an empty report.")

# The CSV is tall format: ItemName | Description | Value
data.columns = data.columns.str.strip()
tall = data.set_index("ItemName")["Value"]

def get(key):
return tall.get(key, None)

def to_float(val):
try:
return float(val)
except (TypeError, ValueError):
return None

raw_date = get("Report Date")
if raw_date is None:
raise EmptyDataError("Could not find Report Date in FFIEC response.")

return [FfiecRiskData(
rssd_id=str(int(float(get("ID_RSSD")))),
report_date=pd.to_datetime(raw_date).date(),
total_assets=to_float(get("RISK2170")),
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Ticker → RSSD ID mapping for FFIEC FR Y-15 filers.

Source: FFIEC NIC FR Y-15 Snapshot Report (December 31, 2024).
https://www.ffiec.gov/npw/FinancialReport/FRY15Reports

Notes:
- All 54 institutions that filed the FR Y-15 as of Q4 2024 are included.
- Foreign banks appear twice: once for the parent (mapped to their NYSE/NASDAQ
ADR ticker) and once for their US intermediate holding company (IHC), which
is the actual FR Y-15 filing entity. Both resolve correctly.
- Pure OTC-traded foreign parents (e.g. BNP Paribas, Societe Generale) are
excluded per the maintainer's scope ("not OTC"). Their US IHCs are still
reachable via RSSD ID directly.
- Flagstar Financial (FLG) was formerly New York Community Bancorp (NYCB).
Both tickers are mapped to the same RSSD for backwards compatibility.
- Discover Financial (DFS) was acquired by Capital One in February 2025 but
was an independent filer as of the December 31, 2024 report date.
"""

# ---------------------------------------------------------------------------
# Primary ticker → RSSD mapping
# ---------------------------------------------------------------------------
TICKER_TO_RSSD: dict[str, str] = {

# ── US G-SIBs ────────────────────────────────────────────────────────────
"JPM": "1039502", # JPMorgan Chase & Co.
"C": "1951350", # Citigroup Inc.
"BK": "3587146", # Bank of New York Mellon Corporation
"STT": "1111435", # State Street Corporation
"BAC": "1073757", # Bank of America Corporation
"WFC": "1120754", # Wells Fargo & Company
"GS": "2380443", # Goldman Sachs Group, Inc.
"MS": "2162966", # Morgan Stanley

# ── Large US Holding Companies ───────────────────────────────────────────
"PNC": "1069778", # PNC Financial Services Group, Inc.
"NTRS": "1199611", # Northern Trust Corporation
"MTB": "1037003", # M&T Bank Corporation
"USB": "1119794", # U.S. Bancorp
"FCNCA": "1075612", # First Citizens BancShares, Inc.
"AXP": "1275216", # American Express Company
"SYF": "4504654", # Synchrony Financial
"FITB": "1070345", # Fifth Third Bancorp
"FLG": "2132932", # Flagstar Financial, Inc. (formerly NYCB)
"NYCB": "2132932", # Flagstar Financial, Inc. (legacy ticker)
"KEY": "1068025", # KeyCorp
"TFC": "1074156", # Truist Financial Corporation
"SCHW": "1026632", # Charles Schwab Corporation
"DFS": "3846375", # Discover Financial Services (independent as of Q4 2024)
"COF": "2277860", # Capital One Financial Corporation
"CFG": "1132449", # Citizens Financial Group, Inc.
"HBAN": "1068191", # Huntington Bancshares Incorporated
"RF": "3242838", # Regions Financial Corporation
"ALLY": "1562859", # Ally Financial Inc.

# ── Foreign Banks — mapped to US Intermediate Holding Company (IHC) ─────
# These are the actual FR Y-15 filing entities registered with the Fed.
# The parent ADR tickers below resolve to the IHC RSSD for data purposes.
"HSBC": "3232316", # HSBC Holdings PLC → HSBC North America Holdings Inc.
"UBS": "4846998", # UBS Group AG → UBS Americas Holding LLC
"SAN": "3981856", # Banco Santander S.A. → Santander Holdings USA, Inc.
"RY": "5280254", # Royal Bank of Canada → RBC US Group Holdings LLC
"BMO": "1245415", # Bank of Montreal → BMO Financial Corp.
"BCS": "5006575", # Barclays PLC → Barclays US LLC
"CM": "5014141", # CIBC → CIBC Bancorp USA Inc.
"SMFG": "3133262", # Sumitomo Mitsui Financial Group, Inc.
"TD": "3606542", # Toronto-Dominion Bank → TD Group US Holdings LLC
"MFG": "5034792", # Mizuho Financial Group → Mizuho Americas LLC
"MUFG": "2961897", # Mitsubishi UFJ Financial Group, Inc.
"DB": "2816906", # Deutsche Bank AG → DB USA Corporation
"BNS": "1238967", # Bank of Nova Scotia
}


# ---------------------------------------------------------------------------
# Reverse map: RSSD → ticker (for display / metadata purposes)
# ---------------------------------------------------------------------------
RSSD_TO_TICKER: dict[str, str] = {
rssd: ticker
for ticker, rssd in TICKER_TO_RSSD.items()
# De-duplicate: keep the most current ticker (e.g. FLG over NYCB)
if ticker not in ("NYCB",)
}


# ---------------------------------------------------------------------------
# Resolver function used by FfiecRiskFetcher
# ---------------------------------------------------------------------------

def resolve_rssd(identifier: str) -> str:
"""Resolve a ticker or raw RSSD ID to a numeric RSSD string.

Args:
identifier: A stock ticker (e.g. 'JPM') or a raw RSSD ID (e.g. '1039502').

Returns:
The RSSD ID as a string.

Raises:
ValueError: If the ticker is not found in the mapping.

Examples:
>>> resolve_rssd("JPM")
'1039502'
>>> resolve_rssd("1039502")
'1039502'
>>> resolve_rssd("jpm") # case-insensitive
'1039502'
"""
upper = identifier.strip().upper()

if upper in TICKER_TO_RSSD:
return TICKER_TO_RSSD[upper]

# If it looks like a numeric RSSD ID, pass it through directly
if identifier.strip().isdigit():
return identifier.strip()

raise ValueError(
f"'{identifier}' is not a recognised ticker or RSSD ID. "
f"Pass a numeric RSSD ID directly, or use one of: {sorted(TICKER_TO_RSSD.keys())}"
)
Loading