0. Process

This report explains how oil and interest rate curve data is produced. The methodology proceeds in the following steps:

  1. Fetch Treasury yield curve. The Python script (curves.py) queries the U.S. Treasury XML feed for daily par yield curve rates (constant-maturity). The feed provides 13 tenors from 1 Month to 30 Years.

  2. Fetch overnight policy rates. The effective Federal Funds Rate (EFFR) and Secured Overnight Financing Rate (SOFR) are retrieved from FRED (Federal Reserve Economic Data) via the public CSV endpoint.

  3. Fetch oil spot price. The latest Brent crude settlement price is fetched from Yahoo Finance (front-month contract BZ=F).

  4. Build oil forward curve. A strip of Brent crude futures contracts is fetched from Yahoo Finance. Standard tenors (1m, 3m, 6m, 12m) are then computed via linear interpolation between the two bracketing contracts.

  5. Build interest rate forward curve. Fed Funds (ZQ) and SOFR (SR1, SR3) futures are fetched from Yahoo Finance. Implied rates are computed as \(100 - \text{settlement price}\).

  6. Store incrementally. Treasury daily yields are saved to Parquet. On each run, only dates not already present are appended.

python curves.py          # Fetch and display
python curves.py --save   # Also save to parquet (incremental)

1. Data Sources

Source Data provided URL Lag
U.S. Treasury Daily par yield curve (13 tenors, 1 Mo – 30 Yr) home.treasury.gov XML feed ~1 business day
FRED Effective Federal Funds Rate (DFF), SOFR fred.stlouisfed.org/graph/fredgraph.csv ~1 business day
Yahoo Finance Brent crude spot (BZ=F) finance.yahoo.com Real-time / EOD
Yahoo Finance Brent crude futures strip (BZK, BZN, BZX, BZF, BZH) finance.yahoo.com Real-time / EOD
Yahoo Finance Fed Funds futures (ZQ), SOFR futures (SR1, SR3) finance.yahoo.com Real-time / EOD

2. Spot Data (Market Prices)

All values in this section are market prices — directly observed, not computed or interpolated.

2.1 Oil Spot

Instrument Price As of Source Classification
Brent Crude (BZ=F) $98.91/bbl 2026-03-13 Yahoo Finance MARKET PRICE

This is the most recent settlement (close) price for the front-month Brent crude futures contract traded on NYMEX.

2.2 Overnight Policy Rates

Rate Value As of Source Classification
Federal Funds (EFFR) 3.64% 2026-03-12 FRED series DFF MARKET PRICE
SOFR 3.65% 2026-03-12 FRED series SOFR MARKET PRICE
  • EFFR (Effective Federal Funds Rate): The volume-weighted median rate at which depository institutions lend reserve balances to each other overnight. Published daily by the Federal Reserve Bank of New York.
  • SOFR (Secured Overnight Financing Rate): The broad measure of the cost of borrowing cash overnight collateralised by U.S. Treasury securities. Published daily by the Federal Reserve Bank of New York.

Both are actual transacted rates, not targets — the current FOMC target range is 3.50%–3.75%.

2.3 Treasury Par Yield Curve

As of 2026-03-13 — Source: U.S. Treasury XML feed
Tenor Yield Classification
1 Mo 3.75% MARKET PRICE
2 Mo 3.71% MARKET PRICE
3 Mo 3.72% MARKET PRICE
4 Mo 3.69% MARKET PRICE
6 Mo 3.70% MARKET PRICE
1 Yr 3.66% MARKET PRICE
2 Yr 3.73% MARKET PRICE
3 Yr 3.74% MARKET PRICE
5 Yr 3.87% MARKET PRICE
7 Yr 4.07% MARKET PRICE
10 Yr 4.28% MARKET PRICE
20 Yr 4.89% MARKET PRICE
30 Yr 4.90% MARKET PRICE

These are constant-maturity Treasury (CMT) rates published daily by the U.S. Department of the Treasury. The Treasury derives them from the daily yield curve for non-inflation-indexed Treasury securities, using closing market bid yields on actively traded securities in the over-the-counter market.

The data is fetched from the Treasury’s XML feed at:

https://home.treasury.gov/resource-center/data-chart-center/interest-rates/pages/xml
  ?data=daily_treasury_yield_curve
  &field_tdr_date_value_month=202603

These are not FRED values. While FRED also publishes these as DGS series (DGS1MO, DGS3MO, etc.), the Treasury XML feed is the primary source and is typically available earlier.


3. Forward Curves

Forward curves reflect market expectations of future prices. They are derived from futures contracts.

3.1 Oil Forward Curve

3.1.1 Raw Futures (Market Prices)

These are actual settlement prices of Brent crude futures contracts at various expiry dates. Each is a market price — the price at which the contract last traded or settled.

Source: Yahoo Finance (BZ=F futures strip)
Ticker Contract Expiry Days Forward Price Classification
BZ=F 2026-04-13 30 $98.91/bbl MARKET PRICE
BZK26.NYM 2026-05-20 67 $103.14/bbl MARKET PRICE
BZN26.NYM 2026-07-20 128 $94.67/bbl MARKET PRICE
BZX26.NYM 2026-11-20 251 $83.64/bbl MARKET PRICE
BZF27.NYM 2027-01-20 312 $80.48/bbl MARKET PRICE
BZH27.NYM 2027-03-20 371 $78.15/bbl MARKET PRICE

3.1.2 Interpolated Standard Tenors (Computed Prices)

The raw futures contracts do not fall on neat 1-month, 3-month, 6-month, and 12-month boundaries. To produce standard tenors, we use linear interpolation between the two bracketing contracts.

Interpolated via linear interpolation from raw futures
Tenor Price Left bracket Right bracket Weight (w) Classification
1m (30d) $98.91/bbl 30d @ $98.91 (exact match) COMPUTED
3m (91d) $99.81/bbl 67d @ $103.14 128d @ $94.67 0.393 COMPUTED
6m (182d) $89.83/bbl 128d @ $94.67 251d @ $83.64 0.439 COMPUTED
12m (365d) $78.39/bbl 312d @ $80.48 371d @ $78.15 0.898 COMPUTED

3.1.3 Interpolation Method: Linear Interpolation

Given two known futures prices at days \(d_0\) and \(d_1\) with prices \(P_0\) and \(P_1\), the price at target day \(d\) is:

\[w = \frac{d - d_0}{d_1 - d_0}\]

\[P(d) = P_0 + w \cdot (P_1 - P_0)\]

This is sometimes called “lerp” (linear interpolation), the most common and transparent interpolation method in quantitative finance. It draws a straight line between two known points and reads off the value at the desired position.

3.1.4 Worked Examples

3-month tenor (target: 91 days)

The two bracketing contracts are:

  • Left: May 2026 contract, 67 days forward, $103.14/bbl
  • Right: Jul 2026 contract, 128 days forward, $94.67/bbl

\[w = \frac{91 - 67}{128 - 67} = \frac{24}{61} = 0.393\]

\[P(91) = \$103.14 + 0.393 \times (\$94.67 - \$103.14) = \$103.14 + 0.393 \times (-\$8.47) = \$103.14 - \$3.33 = \$99.81\]

6-month tenor (target: 182 days)

  • Left: Jul 2026 contract, 128 days forward, $94.67/bbl
  • Right: Nov 2026 contract, 251 days forward, $83.64/bbl

\[w = \frac{182 - 128}{251 - 128} = \frac{54}{123} = 0.439\]

\[P(182) = \$94.67 + 0.439 \times (\$83.64 - \$94.67) = \$94.67 + 0.439 \times (-\$11.03) = \$94.67 - \$4.84 = \$89.83\]

12-month tenor (target: 365 days)

  • Left: Jan 2027 contract, 312 days forward, $80.48/bbl
  • Right: Mar 2027 contract, 371 days forward, $78.15/bbl

\[w = \frac{365 - 312}{371 - 312} = \frac{53}{59} = 0.898\]

\[P(365) = \$80.48 + 0.898 \times (\$78.15 - \$80.48) = \$80.48 + 0.898 \times (-\$2.33) = \$80.48 - \$2.09 = \$78.39\]

1-month tenor (target: 30 days)

The nearest contract is exactly at 30 days ($98.91/bbl), so no interpolation is needed.

3.1.5 Limitations

Linear interpolation assumes the curve between two points is a straight line. It may understate curvature, particularly:

  • Seasonal patterns in oil (e.g. summer driving demand)
  • Contango/backwardation transitions
Alternative interpolation methods
Method Advantage Common usage
Linear (lerp) Simple, transparent, no overshoot Everywhere — default
Cubic spline Smooth curve, captures curvature Yield curve construction
Nelson-Siegel Parsimonious yield curve model (3 params) Central banks, fixed income
Log-linear Better for discount factors Swap curve bootstrapping

3.2 Interest Rate Forward Curve

Source: Yahoo Finance (ZQ, SR1, SR3 futures)
Contract Implied Rate Computation Classification
Fed Funds front 3.62% 100 − futures price MARKET PRICE (derived)
Fed Funds Apr-2026 3.64% 100 − futures price MARKET PRICE (derived)
Fed Funds Jul-2026 3.57% 100 − futures price MARKET PRICE (derived)
Fed Funds Oct-2026 3.50% 100 − futures price MARKET PRICE (derived)
Fed Funds Jan-2027 3.43% 100 − futures price MARKET PRICE (derived)
SOFR 1m front 3.67% 100 − futures price MARKET PRICE (derived)
SOFR 3m front 3.69% 100 − futures price MARKET PRICE (derived)
SOFR 3m Mar-2026 3.69% 100 − futures price MARKET PRICE (derived)
SOFR 3m Jun-2026 3.63% 100 − futures price MARKET PRICE (derived)
SOFR 3m Sep-2026 3.56% 100 − futures price MARKET PRICE (derived)
SOFR 3m Dec-2026 3.50% 100 − futures price MARKET PRICE (derived)

These implied rates are classified as market-price-derived rather than “interpolated,” because each rate corresponds to a specific traded futures contract. The computation is deterministic:

\[\text{implied rate} = 100 - \text{futures settlement price}\]

For example, if the front-month Fed Funds futures (ZQ=F) settles at 96.38, the implied rate is \(100 - 96.38 = 3.62\%\).

This is the standard convention for CME interest rate futures (Fed Funds, SOFR). No interpolation is involved — each contract directly encodes the market’s expectation of the average rate over that contract’s reference period.

Prefix Exchange Underlying
ZQ CBOT 30-Day Federal Funds Rate
SR1 CME 1-Month SOFR
SR3 CME 3-Month SOFR

4. Data Classification Summary

Data point Classification Source Method
Brent crude spot MARKET PRICE Yahoo Finance Last close
Fed Funds (EFFR) MARKET PRICE FRED (DFF) Last published value
SOFR MARKET PRICE FRED (SOFR) Last published value
Treasury yield curve (13 tenors) MARKET PRICE U.S. Treasury XML feed Last published daily curve
Oil futures strip (6 contracts) MARKET PRICE Yahoo Finance Last settlement price
Oil forward 1m, 3m, 6m, 12m COMPUTED (interpolated) Linear interpolation of futures strip Linear interpolation between brackets
Rate futures implied rates (11 contracts) MARKET PRICE (derived) Yahoo Finance (100 − price) Arithmetic: 100 − settlement price

Key distinction: Only the oil forward curve standard tenors (1m, 3m, 6m, 12m) are interpolated. Everything else is either a direct market observation or a deterministic arithmetic transformation of one.


5. Data Storage

Results are stored in Parquet format under curves_data/:

File Contents Update method
treasury_yields.parquet Daily Treasury yield curve (one row per business day, 13 tenor columns) Incremental — only new dates are appended
curves/ (partitioned) Daily snapshot of all spot + forward data (one row per fetch date) Overwrite-or-ignore per year/month partition
## **Treasury yields stored:** 10 dates, from 2026-03-02 to 2026-03-13
Most recent 5 trading days stored in parquet
date 1MONTH 2MONTH 3MONTH 4MONTH 6MONTH 1YEAR 2YEAR 3YEAR 5YEAR 7YEAR 10YEAR 20YEAR 30YEAR
2026-03-13 3.75 3.71 3.72 3.69 3.70 3.66 3.73 3.74 3.87 4.07 4.28 4.89 4.90
2026-03-12 3.76 3.72 3.72 3.69 3.70 3.66 3.76 3.75 3.88 4.06 4.27 4.86 4.88
2026-03-11 3.75 3.70 3.71 3.69 3.68 3.60 3.64 3.64 3.79 3.98 4.21 4.82 4.86
2026-03-10 3.75 3.71 3.71 3.69 3.68 3.56 3.57 3.58 3.73 3.93 4.15 4.74 4.78
2026-03-09 3.75 3.72 3.71 3.68 3.68 3.56 3.56 3.58 3.71 3.90 4.12 4.70 4.72

To inspect the stored data:

library(arrow)
read_parquet("curves_data/treasury_yields.parquet")
python -c "import pandas as pd; print(pd.read_parquet('curves_data/treasury_yields.parquet'))"

Appendix: Source Code

The full source code for curves.py is provided below. Copy and paste to run independently.

Requirements:

pip install yfinance pandas pyarrow requests
```python
"""
curves.py
=========
Fetches and displays oil and interest rate data in a clear structure:

  1. SPOT (latest observed)
     - Latest oil price (Brent front)
     - Latest yield curve (Treasury XML feed — real data, not FRED)

  2. FORWARD CURVES (market expectations)
     - Oil forward curve (1m, 3m, 6m, 12m) — interpolated from futures strip
     - Interest rate forward curve (Fed Funds, SOFR implied from futures)

  3. PARQUET STORAGE
     - Treasury daily yields stored incrementally (only new dates added)

Data sources:
  - Treasury yield curve : home.treasury.gov XML feed (daily, ~1 day lag)
  - Oil futures          : Yahoo Finance (BZ=F strip)
  - Rate futures         : Yahoo Finance (ZQ, SR1, SR3)

Usage:
    pip install yfinance pandas pyarrow requests

    python curves.py          # Fetch and display
    python curves.py --save   # Also save to parquet (incremental)
"""

import argparse
import os
import xml.etree.ElementTree as ET
from datetime import date, timedelta
from pathlib import Path
from typing import Dict, List, Tuple

import pandas as pd
import requests
import yfinance as yf

# Load .env if present
_env_file = Path(__file__).resolve().parent / ".env"
if _env_file.exists():
    for line in _env_file.read_text().splitlines():
        line = line.strip()
        if line and not line.startswith("#") and "=" in line:
            k, _, v = line.partition("=")
            k, v = k.strip(), v.strip().strip('"').strip("'")
            if k and k not in os.environ:
                os.environ[k] = v

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

PARQUET_DIR = Path("./curves_data")

FRED_CSV = "https://fred.stlouisfed.org/graph/fredgraph.csv"

# Treasury daily par yield curve XML feed
# https://home.treasury.gov/treasury-daily-interest-rate-xml-feed
TREASURY_XML = "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/pages/xml"
# XML element names -> display labels
TREASURY_TENORS = [
    ("BC_1MONTH",  "1 Mo"),
    ("BC_2MONTH",  "2 Mo"),
    ("BC_3MONTH",  "3 Mo"),
    ("BC_4MONTH",  "4 Mo"),
    ("BC_6MONTH",  "6 Mo"),
    ("BC_1YEAR",   "1 Yr"),
    ("BC_2YEAR",   "2 Yr"),
    ("BC_3YEAR",   "3 Yr"),
    ("BC_5YEAR",   "5 Yr"),
    ("BC_7YEAR",   "7 Yr"),
    ("BC_10YEAR",  "10 Yr"),
    ("BC_20YEAR",  "20 Yr"),
    ("BC_30YEAR",  "30 Yr"),
]

# CME month codes: F=Jan, G=Feb, H=Mar, J=Apr, K=May, M=Jun, N=Jul, Q=Aug, U=Sep, V=Oct, X=Nov, Z=Dec
OIL_MONTH = {"F": 1, "G": 2, "H": 3, "J": 4, "K": 5, "M": 6, "N": 7, "Q": 8, "U": 9, "V": 10, "X": 11, "Z": 12}

# Oil strip (Brent)
def _oil_strip(year: int):
    y2, y2n = str(year)[-2:], str(year + 1)[-2:]
    return {
        "BZ=F": "Front",
        f"BZK{y2}.NYM": "May",
        f"BZN{y2}.NYM": "Jul",
        f"BZX{y2}.NYM": "Nov",
        f"BZF{y2n}.NYM": "Jan",
        f"BZH{y2n}.NYM": "Mar",
    }

# Rate futures: Fed Funds (ZQ) + SOFR (SR1, SR3). Price = 100 - implied_rate.
def _rate_futures_strip(year: int):
    y2, y2n = str(year)[-2:], str(year + 1)[-2:]
    return {
        "ZQ=F": "Fed Funds front",
        f"ZQJ{y2}.CBT": f"Fed Funds Apr-{year}",
        f"ZQN{y2}.CBT": f"Fed Funds Jul-{year}",
        f"ZQV{y2}.CBT": f"Fed Funds Oct-{year}",
        f"ZQF{y2n}.CBT": f"Fed Funds Jan-{year+1}",
        "SR1=F": "SOFR 1m front",
        "SR3=F": "SOFR 3m front",
        f"SR3H{y2}.CME": f"SOFR 3m Mar-{year}",
        f"SR3M{y2}.CME": f"SOFR 3m Jun-{year}",
        f"SR3U{y2}.CME": f"SOFR 3m Sep-{year}",
        f"SR3Z{y2}.CME": f"SOFR 3m Dec-{year}",
    }


# ---------------------------------------------------------------------------
# Fetch: Treasury yield curve (XML feed — real data from Treasury)
# ---------------------------------------------------------------------------

NS = {"d": "http://schemas.microsoft.com/ado/2007/08/dataservices",
      "m": "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
      "a": "http://www.w3.org/2005/Atom"}


def fetch_treasury_xml(year_month: str = None) -> pd.DataFrame:
    """
    Fetch daily Treasury par yield curve from XML feed.
    year_month: "202603" for a specific month, or None for current year.
    Returns DataFrame with columns: date, BC_1MONTH, BC_2MONTH, … BC_30YEAR.
    All values are REAL data published by Treasury (not interpolated by us).
    """
    if year_month:
        params = {"data": "daily_treasury_yield_curve", "field_tdr_date_value_month": year_month}
    else:
        params = {"data": "daily_treasury_yield_curve", "field_tdr_date_value": str(date.today().year)}
    r = requests.get(TREASURY_XML, params=params, timeout=30)
    r.raise_for_status()
    root = ET.fromstring(r.content)
    rows = []
    for entry in root.findall("a:entry", NS):
        props = entry.find(".//m:properties", NS)
        if props is None:
            continue
        dt_el = props.find("d:NEW_DATE", NS)
        if dt_el is None or dt_el.text is None:
            continue
        row = {"date": dt_el.text[:10]}
        for xml_key, _ in TREASURY_TENORS:
            el = props.find(f"d:{xml_key}", NS)
            if el is not None and el.text:
                try:
                    row[xml_key] = float(el.text)
                except ValueError:
                    pass
        rows.append(row)
    df = pd.DataFrame(rows)
    if not df.empty:
        df["date"] = pd.to_datetime(df["date"])
        df = df.sort_values("date")
    return df


def fetch_yield_curve() -> Tuple[dict, str]:
    """
    Latest Treasury yield curve from XML feed.
    Returns (dict of label->value, date_str). All values are real Treasury data.
    """
    today = date.today()
    ym = today.strftime("%Y%m")
    df = fetch_treasury_xml(ym)
    if df.empty:
        prev = (today.replace(day=1) - timedelta(days=1))
        df = fetch_treasury_xml(prev.strftime("%Y%m"))
    if df.empty:
        return {}, "N/A"
    row = df.iloc[-1]
    dt_str = row["date"].strftime("%Y-%m-%d")
    result = {}
    for xml_key, label in TREASURY_TENORS:
        if xml_key in row and pd.notna(row[xml_key]):
            result[label] = row[xml_key]
    return result, dt_str


def fetch_latest_oil() -> Tuple[float, str]:
    """Latest Brent crude price ($/bbl). REAL market close price."""
    t = yf.Ticker("BZ=F")
    hist = t.history(period="5d")
    if hist.empty:
        return 0.0, "N/A"
    price = float(hist["Close"].iloc[-1])
    dt = hist.index[-1].strftime("%Y-%m-%d")
    return price, dt


def _fred_series(series_id: str) -> Tuple[float, str]:
    """Fetch latest value for a single FRED series via public CSV endpoint."""
    from io import StringIO
    try:
        r = requests.get(FRED_CSV, params={"id": series_id}, timeout=20)
        r.raise_for_status()
        df = pd.read_csv(StringIO(r.text))
        date_col = next((c for c in ["DATE", "Date", "date"] if c in df.columns), df.columns[0])
        if series_id not in df.columns:
            return float("nan"), "N/A"
        df = df[df[series_id].astype(str) != "."].dropna(subset=[series_id])
        if df.empty:
            return float("nan"), "N/A"
        df[date_col] = pd.to_datetime(df[date_col])
        row = df.iloc[-1]
        return float(row[series_id]), row[date_col].strftime("%Y-%m-%d")
    except Exception:
        return float("nan"), "N/A"


def fetch_policy_rates() -> dict:
    """
    Fetch actual policy rates from FRED:
      - DFF  : Effective Federal Funds Rate (daily, ~1 day lag)
      - SOFR : Secured Overnight Financing Rate (daily, ~1 day lag)
    These are REAL observed rates, not futures-implied.
    """
    result = {}
    for series_id, label in [("DFF", "Fed Funds (EFFR)"), ("SOFR", "SOFR")]:
        val, dt = _fred_series(series_id)
        result[label] = {"value": val, "as_of": dt}
    return result


# ---------------------------------------------------------------------------
# Fetch: Forward curves
# ---------------------------------------------------------------------------

def _contract_date(ticker: str, fetch_date: date):
    """Approx expiry from ticker. BZK26.NYM, ZQJ26.CBT, SR1J26.CME."""
    if "=F" in ticker:
        return fetch_date + timedelta(days=30)
    try:
        if ticker.startswith("BZ") and len(ticker) >= 5:
            m = OIL_MONTH.get(ticker[2])
            y = 2000 + int(ticker[3:5])
        elif ticker.startswith("ZQ") and len(ticker) >= 5:
            m = OIL_MONTH.get(ticker[2])
            y = 2000 + int(ticker[3:5])
        elif ticker.startswith("SR") and len(ticker) >= 6:
            m = OIL_MONTH.get(ticker[3])
            y = 2000 + int(ticker[4:6])
        else:
            return None
        return date(y, m, 20) if m else None
    except (IndexError, ValueError):
        return None


def fetch_oil_forward(fetch_date: date) -> Tuple[List[Tuple[date, float]], list]:
    """
    Oil forward curve.
    Returns:
        raw_points : [(expiry_date, price), ...] -- REAL settlement prices
        interpolated: [{"tenor", "price", "method"}, ...]  -- standard tenors with formulas
    """
    strip = _oil_strip(fetch_date.year)
    points = []
    for ticker, _ in strip.items():
        try:
            hist = yf.Ticker(ticker).history(period="5d")
            if hist.empty:
                continue
            price = float(hist["Close"].iloc[-1])
            exp = _contract_date(ticker, fetch_date)
            if exp and (exp - fetch_date).days > 0:
                points.append((exp, price))
        except Exception:
            continue
    if len(points) < 2:
        return [], []
    points.sort()
    days_arr = [(p[0] - fetch_date).days for p in points]
    price_arr = [p[1] for p in points]
    tenors_days = {"1m": 30, "3m": 91, "6m": 182, "12m": 365}
    result = []
    for label, days in tenors_days.items():
        if days <= days_arr[0]:
            result.append({"tenor": label, "price": price_arr[0],
                           "method": f"(nearest: {days_arr[0]}d = ${price_arr[0]:.2f})"})
        elif days >= days_arr[-1]:
            result.append({"tenor": label, "price": price_arr[-1],
                           "method": f"(nearest: {days_arr[-1]}d = ${price_arr[-1]:.2f})"})
        else:
            for i in range(len(days_arr) - 1):
                if days_arr[i] <= days <= days_arr[i + 1]:
                    w = (days - days_arr[i]) / (days_arr[i + 1] - days_arr[i])
                    p = price_arr[i] + w * (price_arr[i + 1] - price_arr[i])
                    result.append({
                        "tenor": label, "price": p,
                        "method": (f"(lerp: w=({days}-{days_arr[i]})/({days_arr[i+1]}-{days_arr[i]})="
                                   f"{w:.3f}, ${price_arr[i]:.2f}+{w:.3f}*"
                                   f"(${price_arr[i+1]:.2f}-${price_arr[i]:.2f})=${p:.2f})")
                    })
                    break
    return points, result


def fetch_rate_forward(fetch_date: date) -> List[Tuple[str, float]]:
    """Interest rate forward curve: [(contract, implied %), ...]. Implied = 100 - price."""
    strip = _rate_futures_strip(fetch_date.year)
    result = []
    for ticker, label in strip.items():
        try:
            hist = yf.Ticker(ticker).history(period="5d")
            if hist.empty:
                continue
            price = float(hist["Close"].iloc[-1])
            implied = 100 - price
            result.append((label, implied))
        except Exception:
            continue
    return result


# ---------------------------------------------------------------------------
# Display
# ---------------------------------------------------------------------------

def display(data: dict):
    """Print all data with clear annotations on what is REAL vs INTERPOLATED."""
    fd = data["fetch_date"]
    print()
    print("=" * 70)
    print(f"  OIL & INTEREST RATE CURVES -- {fd}")
    print("=" * 70)

    # ── SPOT: Oil ──
    oil_price, oil_dt = data["oil_spot"]
    print()
    print("+-- SPOT (Latest observed) -------")
    print("|")
    print(f"|  Brent crude (BZ=F)     ${oil_price:.2f}/bbl     as of {oil_dt}")
    print("|  [REAL: market close price from Yahoo Finance]")
    print("|")

    # ── SPOT: Policy rates ──
    pr = data.get("policy_rates", {})
    print("|  Overnight policy rates")
    print("|  [REAL: actual observed rates from FRED (DFF, SOFR)]")
    print("|")
    for lbl in ["Fed Funds (EFFR)", "SOFR"]:
        info = pr.get(lbl, {})
        val = info.get("value", float("nan"))
        dt = info.get("as_of", "N/A")
        if pd.notna(val):
            print(f"|     {lbl:<20} {val:.2f}%     as of {dt}")
        else:
            print(f"|     {lbl:<20} N/A")
    print("|")

    # ── SPOT: Treasury yield curve ──
    yc = data["yield_curve"]
    yc_dt = data.get("yield_curve_date", "N/A")
    print(f"|  Treasury par yield curve   (as of {yc_dt})")
    print("|  [REAL: published daily by U.S. Treasury, constant-maturity rates]")
    print("|")
    for label in ["1 Mo", "2 Mo", "3 Mo", "4 Mo", "6 Mo", "1 Yr", "2 Yr",
                   "3 Yr", "5 Yr", "7 Yr", "10 Yr", "20 Yr", "30 Yr"]:
        if label in yc:
            print(f"|     {label:<6}  {yc[label]:.2f}%")
    print("+--")
    print()

    # ── FORWARD: Oil ──
    oil_fwd = data["oil_forward"]
    oil_raw = data.get("oil_forward_raw", [])
    print("+-- OIL FORWARD CURVE (Market expectations) -------")
    print("|")
    if oil_raw:
        print("|  Raw futures (REAL: settlement prices from Yahoo Finance):")
        for exp, price in oil_raw:
            days = (exp - date.fromisoformat(fd)).days
            print(f"|     {exp} ({days:>3}d)   ${price:.2f}/bbl")
    if oil_fwd:
        print("|")
        print("|  Standard tenors (INTERPOLATED via linear interpolation):")
        for rec in oil_fwd:
            print(f"|     {rec['tenor']:>4}   ${rec['price']:.2f}/bbl   {rec['method']}")
    else:
        print("|     (no data)")
    print("+--")
    print()

    # ── FORWARD: Rates ──
    rate_fwd = data["rate_forward"]
    print("+-- INTEREST RATE FORWARD CURVE -------")
    print("|  [REAL: implied rate = 100 - futures settlement price]")
    print("|")
    if rate_fwd:
        for label, rate in rate_fwd:
            print(f"|     {label:<25}  {rate:.2f}%")
    else:
        print("|     (no data)")
    print("+--")
    print()


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def _log(msg):
    import sys
    sys.stderr.write(msg + "\n")
    sys.stderr.flush()


def _existing_treasury_dates(path: Path) -> set:
    """Read existing treasury yield parquet and return set of dates already stored."""
    pf = path / "treasury_yields.parquet"
    if not pf.exists():
        return set()
    try:
        df = pd.read_parquet(pf)
        return set(pd.to_datetime(df["date"]).dt.strftime("%Y-%m-%d"))
    except Exception:
        return set()


def _save_treasury_incremental(df: pd.DataFrame, path: Path):
    """Append only new dates to the treasury yields parquet file."""
    pf = path / "treasury_yields.parquet"
    if pf.exists():
        try:
            existing = pd.read_parquet(pf)
            existing["date"] = pd.to_datetime(existing["date"])
            df["date"] = pd.to_datetime(df["date"])
            existing_dates = set(existing["date"].dt.strftime("%Y-%m-%d"))
            new_rows = df[~df["date"].dt.strftime("%Y-%m-%d").isin(existing_dates)]
            if new_rows.empty:
                _log(f"       Treasury yields: 0 new dates (all {len(existing_dates)} already stored)")
                return 0
            combined = pd.concat([existing, new_rows], ignore_index=True)
            combined = combined.sort_values("date")
            combined.to_parquet(pf, index=False)
            _log(f"       Treasury yields: +{len(new_rows)} new dates (total: {len(combined)})")
            return len(new_rows)
        except Exception as e:
            _log(f"       Warning re-reading parquet: {e}, overwriting")
    df.to_parquet(pf, index=False)
    _log(f"       Treasury yields: saved {len(df)} dates (new file)")
    return len(df)


def _save_curves_snapshot(data: dict, fetch_date: date, path: Path):
    """Save daily snapshot of oil/rate curves to parquet (one row per fetch_date)."""
    import pyarrow as pa
    import pyarrow.parquet as pq

    record = {"fetch_date": fetch_date.isoformat(), "oil_spot": data["oil_spot"][0]}
    for label, val in data["yield_curve"].items():
        record[f"yc_{label.replace(' ', '_')}"] = val
    for label, info in data.get("policy_rates", {}).items():
        key = label.replace(" ", "_").replace("(", "").replace(")", "")
        record[f"spot_{key}"] = info.get("value")
    for rec in data["oil_forward"]:
        record[f"oil_fwd_{rec['tenor']}"] = rec["price"]
    for label, rate in data["rate_forward"]:
        key = label.replace(" ", "_").replace("-", "_")[:30]
        record[f"rate_fwd_{key}"] = rate

    df = pd.DataFrame([record])
    df["fetch_date"] = pd.to_datetime(df["fetch_date"])
    df["fetch_year"] = df["fetch_date"].dt.year
    df["fetch_month"] = df["fetch_date"].dt.month
    curves_path = path / "curves"
    pq.write_to_dataset(
        pa.Table.from_pandas(df),
        root_path=str(curves_path),
        partition_cols=["fetch_year", "fetch_month"],
        existing_data_behavior="overwrite_or_ignore",
    )
    _log(f"       Curves snapshot saved to {curves_path}/")


def run(save: bool = False):
    fetch_date = date.today()

    _log("[1/6] Fetching Treasury yield curve (XML feed) ...")
    yield_curve, yc_date = fetch_yield_curve()
    _log(f"       {len(yield_curve)} tenors as of {yc_date}")

    _log("[2/6] Fetching policy rates (FRED: EFFR + SOFR) ...")
    policy_rates = fetch_policy_rates()
    for lbl, info in policy_rates.items():
        v = f"{info['value']:.2f}%" if pd.notna(info["value"]) else "N/A"
        _log(f"       {lbl}: {v} (as of {info['as_of']})")

    _log("[3/6] Fetching latest oil price ...")
    oil_price, oil_dt = fetch_latest_oil()
    _log(f"       Brent ${oil_price:.2f} (as of {oil_dt})")

    _log("[4/6] Fetching oil forward curve ...")
    oil_raw, oil_forward = fetch_oil_forward(fetch_date)
    _log(f"       {len(oil_raw)} contracts, {len(oil_forward)} interpolated tenors")

    _log("[5/6] Fetching rate futures (Fed Funds, SOFR) ...")
    rate_forward = fetch_rate_forward(fetch_date)
    _log(f"       {len(rate_forward)} contracts")

    data = {
        "fetch_date": fetch_date.isoformat(),
        "oil_spot": (oil_price, oil_dt),
        "yield_curve": yield_curve,
        "yield_curve_date": yc_date,
        "policy_rates": policy_rates,
        "oil_forward_raw": oil_raw,
        "oil_forward": oil_forward,
        "rate_forward": rate_forward,
    }

    display(data)

    if save:
        _log("[6/6] Saving to parquet (incremental) ...")
        PARQUET_DIR.mkdir(parents=True, exist_ok=True)

        existing_dates = _existing_treasury_dates(PARQUET_DIR)
        _log(f"       Existing Treasury dates in parquet: {len(existing_dates)}")

        today_ym = fetch_date.strftime("%Y%m")
        treas_df = fetch_treasury_xml(today_ym)
        if treas_df.empty:
            prev = (fetch_date.replace(day=1) - timedelta(days=1))
            treas_df = fetch_treasury_xml(prev.strftime("%Y%m"))
        if not treas_df.empty:
            _save_treasury_incremental(treas_df, PARQUET_DIR)

        _save_curves_snapshot(data, fetch_date, PARQUET_DIR)
        _log("")
        print(f"  Data saved to {PARQUET_DIR}/\n")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Oil & interest rate curves (spot + forward)")
    parser.add_argument("--save", action="store_true", help="Save to parquet")
    args = parser.parse_args()
    run(save=args.save)

```