Industrial Production Signal

A reproducible specification combining INDPRO (US Industrial Production) and RIWG21222S (Mining share of GDP) into a monthly, rules‑based demand/supply signal for cyclical and metals exposures.

Abstract

This document formalises a two‑factor signal: (i) industrial demand via INDPRO year‑over‑year growth and (ii) a proxy for primary supply pressure via the mining value‑added share of GDP (RIWG21222S). Series are aligned to a monthly index and transformed to YoY rates. Regimes—Bullish, Neutral, and Bearish—are assigned with transparent thresholds oriented to silver/cyclical risk.

1. Data

Each series is fetched from FRED with title metadata. Values are coerced to numeric and reindexed to a common monthly calendar.

2. Alignment & Transformations

  1. Monthly alignment: set Date as index and reindex each series to MS (month‑start), forward‑filling within series.
  2. Merge panel: concatenate aligned series and reset the index to a Date column.
  3. Rates of change: compute 12‑month percentage changes for both inputs:
    INDPRO_{YoY,t} = 100 \times \frac{INDPRO_t - INDPRO_{t-12}}{INDPRO_{t-12}}
    RIWG_{YoY,t} = 100 \times \frac{RIWG_t - RIWG_{t-12}}{RIWG_{t-12}}

3. Signal Classification

Define thresholds consistent with a demand‑versus‑supply framing for metals:

  • Bullish: INDPRO_{YoY} > +3% and RIWG_{YoY} ≤ 0% (robust demand, no rising mining share).
  • Bearish: INDPRO_{YoY} < 0% and RIWG_{YoY} > 0% (weak demand alongside rising mining share).
  • Neutral: otherwise.

Missing inputs default to Neutral. Thresholds are illustrative and can be calibrated via backtests.

4. Implementation Notes (Python)

def fetch_fred_series_df(series_id):
    try:
        s = fred.get_series(series_id)
        df = s.reset_index(); df.columns = ['Date','Value']
        df['Series_ID'] = series_id
        try:
            meta = fred.get_series_info(series_id)
            df['title'] = getattr(meta, 'title', None)
        except Exception:
            df['title'] = None
        df['Date'] = pd.to_datetime(df['Date'])
        df['Value'] = pd.to_numeric(df['Value'], errors='coerce')
        return df[['Series_ID','title','Date','Value']]
    except Exception:
        return pd.DataFrame(columns=['Series_ID','title','Date','Value'])

# Fetch
indpro_df     = fetch_fred_series_df('INDPRO')
riwg21222s_df = fetch_fred_series_df('RIWG21222S')

# Monthly alignment (MS) & forward-fill
for d in (indpro_df, riwg21222s_df):
    d['Date'] = pd.to_datetime(d['Date'])
    d.set_index('Date', inplace=True)
    d['Value'] = pd.to_numeric(d['Value'], errors='coerce')

IND = indpro_df['Value'].reindex(pd.date_range(indpro_df.index.min(), indpro_df.index.max(), freq='MS')).ffill()
RIW = riwg21222s_df['Value'].reindex(pd.date_range(riwg21222s_df.index.min(), riwg21222s_df.index.max(), freq='MS')).ffill()

merged = pd.concat([IND, RIW], axis=1)
merged.columns = ['Value_INDPRO','Value_RIWG21222S']
merged = merged.reset_index().rename(columns={'index':'Date'})

# YoY transforms
merged['INDPRO_YoY']     = merged['Value_INDPRO'].pct_change(12) * 100
merged['RIWG21222S_YoY'] = merged['Value_RIWG21222S'].pct_change(12) * 100

# Signal rules
def generate_industrial_mining_signal(row):
    indpro_yoy = row['INDPRO_YoY']
    riwg_yoy   = row['RIWG21222S_YoY']
    if pd.isna(indpro_yoy) or pd.isna(riwg_yoy):
        return 'Neutral'
    if (indpro_yoy > 3) and (riwg_yoy <= 0):
        return 'Bullish'
    if (indpro_yoy < 0) and (riwg_yoy > 0):
        return 'Bearish'
    return 'Neutral'

merged['Industrial_Mining_Signal'] = merged.apply(generate_industrial_mining_signal, axis=1)

5. Assumptions & Limitations

6. Reproducibility

7. Applications

Useful as a demand‑supply overlay for metals (e.g., silver) and cyclicals, and as an explanatory variable in macro factor models. Naturally complements liquidity and real‑yield signals for regime triangulation.