Chris Parmer — home

WTI Crude Oil futures term structure

Commodities — contango vs backwardation

Example from the compendium of canonical charts

Commodities — WTI Crude Oil futures term structure (contango vs backwardation)

Python Code

"""Commodities — WTI Crude Oil futures term structure (contango vs backwardation)."""


import numpy as np
import pandas as pd
import plotly.graph_objects as go
import warnings
warnings.filterwarnings("ignore")


# WTI Crude contracts: try CME/NYMEX symbols
# 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
CONTRACTS_NYM = [
    ("CL=F",   "Spot/Front"),
    ("CLN25.NYM", "Jul 2025"),
    ("CLQ25.NYM", "Aug 2025"),
    ("CLU25.NYM", "Sep 2025"),
    ("CLV25.NYM", "Oct 2025"),
    ("CLX25.NYM", "Nov 2025"),
    ("CLZ25.NYM", "Dec 2025"),
    ("CLF26.NYM", "Jan 2026"),
    ("CLG26.NYM", "Feb 2026"),
    ("CLH26.NYM", "Mar 2026"),
]

# Alternative symbols without suffix
CONTRACTS_ALT = [
    ("CLN25=F", "Jul 2025"),
    ("CLQ25=F", "Aug 2025"),
    ("CLU25=F", "Sep 2025"),
    ("CLV25=F", "Oct 2025"),
    ("CLX25=F", "Nov 2025"),
    ("CLZ25=F", "Dec 2025"),
    ("CLF26=F", "Jan 2026"),
    ("CLG26=F", "Feb 2026"),
]


def fetch_price(sym):
    """Try to get last closing price for a futures contract."""
    try:
        import yfinance as yf
        hist = yf.Ticker(sym).history(period="5d")["Close"].dropna()
        if len(hist) > 0:
            return float(hist.iloc[-1])
    except Exception:
        pass
    return None


def fetch_term_structure():
    """Fetch WTI term structure from yfinance."""
    try:
        import yfinance as yf
        results = []

        # Try primary symbols first
        for sym, label in CONTRACTS_NYM:
            price = fetch_price(sym)
            if price and price > 0:
                results.append({"label": label, "price": price, "symbol": sym})

        # If sparse, try alt symbols
        if len(results) < 4:
            for sym, label in CONTRACTS_ALT:
                if not any(r["label"] == label for r in results):
                    price = fetch_price(sym)
                    if price and price > 0:
                        results.append({"label": label, "price": price, "symbol": sym})

        if len(results) < 3:
            raise ValueError(f"only {len(results)} contracts found")

        # Sort by label (they're in order already)
        df = pd.DataFrame(results)
        return df

    except Exception as e:
        print(f"  futures fetch failed ({e}), using synthetic data")
        return None


def make_synthetic():
    """Synthetic WTI term structure — mild contango."""
    rng = np.random.default_rng(42)
    months = ["Spot", "Jul'25", "Aug'25", "Sep'25", "Oct'25", "Nov'25",
              "Dec'25", "Jan'26", "Feb'26", "Mar'26"]
    # Slight contango: prices rise modestly with time
    base = 72.50
    prices = [base + i * 0.35 + rng.normal(0, 0.15) for i in range(len(months))]
    return pd.DataFrame({"label": months, "price": prices})


def build_fig(df):
    prices = df["price"].tolist()
    labels = df["label"].tolist()

    # Determine market structure
    slope = (prices[-1] - prices[0]) / max(len(prices) - 1, 1)
    if slope > 0.05:
        structure = "Contango (market in surplus — deferred prices above spot)"
        line_color = TEAL
    elif slope < -0.05:
        structure = "Backwardation (tight supply — spot premium over deferred)"
        line_color = GREEN
    else:
        structure = "Flat term structure"
        line_color = VIOLET

    fig = go.Figure()

    # Shaded band between spot and last
    fig.add_trace(go.Scatter(
        x=labels, y=prices,
        mode="lines+markers",
        name="WTI Crude",
        line=dict(color=line_color, width=2.5),
        marker=dict(size=9, color=line_color, symbol="circle"),
        hovertemplate="%{x}: $%{y:.2f}/bbl<extra></extra>",
    ))

    # Annotate spot price
    fig.add_annotation(
        x=labels[0], y=prices[0],
        text=f"Spot<br>${prices[0]:.2f}",
        showarrow=True,
        arrowhead=2,
        arrowcolor=line_color,
        ax=30, ay=-40,
        font=dict(size=11),
    )

    # Annotate farthest contract
    fig.add_annotation(
        x=labels[-1], y=prices[-1],
        text=f"{labels[-1]}<br>${prices[-1]:.2f}",
        showarrow=True,
        arrowhead=2,
        arrowcolor=line_color,
        ax=-30, ay=-40,
        font=dict(size=11),
    )

    fig.add_annotation(
        x=0.5, y=1.04,
        xref="paper", yref="paper",
        text=structure,
        showarrow=False,
        font=dict(size=11, color=line_color),
        align="center",
    )

    fig.update_layout(
        xaxis_title="Contract Month",
        yaxis_title="Price (USD/bbl)",
        showlegend=False,
        margin=dict(t=80, b=60, l=60, r=20),
    )
    return fig


def generate():
    df = fetch_term_structure()
    if df is None or len(df) < 3:
        df = make_synthetic()
        print("  using synthetic WTI term structure")
    else:
        print(f"  fetched {len(df)} WTI contracts from yfinance")

    fig = build_fig(df)
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly