Skip to main content
⚠️
The macro signal is a monitoring tool, not a trading system. It reflects historical correlations, not guaranteed future outcomes. Not financial advice.
Fixed Income · Part 5 of 5

Macro Signal Generator

Four fixed income signals. One daily verdict. OpenClaw combines the yield curve, credit spreads, real rates, and inflation breakevens into a single 🟢 Green / 🟡 Yellow / 🔴 Red macro read — delivered to your inbox every morning.

4 Signals Combined
Traffic Light Output
Daily Email
Full Config-Driven

Why Combine All Four Signals?

Each of the four signals we've built tells you something different about the economy:

No single signal is reliable alone. But when multiple signals align — when the yield curve is inverted AND spreads are widening AND real rates are elevated — the macro picture is significantly clearer.

The macro signal generator combines all four into one daily output: Green (conditions broadly healthy), Yellow (mixed signals, watch carefully), or Red (multiple indicators flashing concern).

How the Scoring Works

Each of the four signals gets scored on a simple -1 / 0 / +1 scale:

The four scores are averaged. The final verdict:

Scoring Criteria Table

Signal Green (+1) Yellow (0) Red (-1)
2s10s Spread > +0.25% -0.25% to +0.25% < -0.25% (inverted)
HY Credit Spread < 400 bps 400–600 bps > 600 bps
10yr Real Rate < 1.5% 1.5% – 2.5% > 2.5%
10yr Breakeven 1.75% – 2.5% 2.5% – 2.75% > 2.75% or < 1.75%

The Complete fi-config.yaml

Here's the full config that drives the entire Fixed Income series:

fi-config.yaml — Complete OpenClaw Fixed Income Config
# fi-config.yaml — Complete OpenClaw Fixed Income Config
# This single file drives all five modules in the Fixed Income series

fred:
  api_key: "your_fred_api_key_here"
  # Free key: https://fred.stlouisfed.org/docs/api/api_key.html

# ── Yield curve ───────────────────────────────────────────────────
yield_curve:
  maturities:
    - { label: "2-Year",  series: "DGS2"  }
    - { label: "5-Year",  series: "DGS5"  }
    - { label: "10-Year", series: "DGS10" }
    - { label: "30-Year", series: "DGS30" }
  alerts:
    inversion: true
    spread_threshold: -0.25

# ── Credit spreads ────────────────────────────────────────────────
credit_spreads:
  investment_grade: "BAMLC0A0CM"
  high_yield:       "BAMLH0A0HYM2"
  alerts:
    ig_weekly_move:  50
    hy_weekly_move: 150
    hy_danger_level: 700

# ── Rates ─────────────────────────────────────────────────────────
rates:
  fed_funds:  "FEDFUNDS"
  sofr:       "SOFR"
  real_rate:  "DFII10"
  ten_year:   "DGS10"
  alerts:
    real_rate_threshold: 2.5

# ── Breakevens ────────────────────────────────────────────────────
breakevens:
  five_year:  "T5YIE"
  ten_year:   "T10YIE"
  forward:    "T5YIFR"
  cpi_series: "CPIAUCSL"
  alerts:
    weekly_move: 0.15
    above_fed_target: 2.75

# ── Macro signal scoring thresholds ──────────────────────────────
macro_signal:
  yield_curve:
    green: 0.25      # 2s10s spread above this = green
    red:  -0.25      # 2s10s spread below this = red
  hy_spread_bps:
    green: 400
    red:   600
  real_rate:
    green: 1.5
    red:   2.5
  breakeven_10yr:
    green_min: 1.75
    green_max: 2.50
    red_high:  2.75
    red_low:   1.75

# ── Alert delivery ────────────────────────────────────────────────
alerts:
  email: "you@example.com"
  daily_digest: true
  send_time: "07:00"

# ── Scheduler ─────────────────────────────────────────────────────
scheduler:
  morning_brief:
    script: fi_daily_brief.py
    schedule: "0 7 * * 1-5"

This is the unified config that every part of the Fixed Income series consumes. One key, one config, five separate but coordinated monitoring modules.

The Macro Signal Script

Here's the Python script that combines all four signals into one daily verdict:

fi_daily_brief.py — OpenClaw Fixed Income Daily Brief
# fi_daily_brief.py — OpenClaw Fixed Income Daily Brief
# Combines all four signals into one macro verdict

import yaml
from fredapi import Fred
import pandas as pd
import smtplib
from email.mime.text import MIMEText
from datetime import datetime

def load_config(path: str = "fi-config.yaml") -> dict:
    with open(path) as f:
        return yaml.safe_load(f)

cfg  = load_config()
fred = Fred(api_key=cfg["fred"]["api_key"])
MS   = cfg["macro_signal"]

# ── Fetch all four signals ────────────────────────────────────────
def fetch_all_signals() -> dict:
    """
    Pulls the latest values for all four signal categories.
    Returns a dict with current values for scoring.
    """
    signals = {}

    # 1. Yield curve — 2s10s spread
    try:
        two_yr = float(fred.get_series("DGS2").dropna().iloc[-1])
        ten_yr = float(fred.get_series("DGS10").dropna().iloc[-1])
        signals["spread_2s10s"] = round(ten_yr - two_yr, 3)
        signals["two_year"]  = two_yr
        signals["ten_year"]  = ten_yr
    except Exception as e:
        signals["spread_2s10s"] = 0

    # 2. High yield credit spread
    try:
        hy = fred.get_series("BAMLH0A0HYM2").dropna().iloc[-1]
        signals["hy_spread_bps"] = round(float(hy) * 100, 1)
    except Exception:
        signals["hy_spread_bps"] = 500

    # 3. Real rate
    try:
        rr = fred.get_series("DFII10").dropna().iloc[-1]
        signals["real_rate"] = round(float(rr), 3)
    except Exception:
        signals["real_rate"] = 1.0

    # 4. 10yr breakeven
    try:
        be = fred.get_series("T10YIE").dropna().iloc[-1]
        signals["breakeven_10yr"] = round(float(be), 3)
    except Exception:
        signals["breakeven_10yr"] = 2.3

    return signals

# ── Score each signal ─────────────────────────────────────────────
def score_signals(signals: dict) -> dict:
    """
    Scores each signal on a -1 (red) / 0 (yellow) / +1 (green) scale.
    """
    scores = {}

    # Yield curve
    spread = signals["spread_2s10s"]
    if spread > MS["yield_curve"]["green"]:
        scores["yield_curve"] = (1, "🟢", f"2s10s at {spread:+.3f}% — normal, positive slope")
    elif spread < MS["yield_curve"]["red"]:
        scores["yield_curve"] = (-1, "🔴", f"2s10s at {spread:+.3f}% — INVERTED")
    else:
        scores["yield_curve"] = (0, "🟡", f"2s10s at {spread:+.3f}% — near flat, watch closely")

    # HY credit spreads
    hy = signals["hy_spread_bps"]
    if hy < MS["hy_spread_bps"]["green"]:
        scores["credit_spreads"] = (1, "🟢", f"HY spreads {hy:.0f} bps — normal range")
    elif hy > MS["hy_spread_bps"]["red"]:
        scores["credit_spreads"] = (-1, "🔴", f"HY spreads {hy:.0f} bps — elevated stress")
    else:
        scores["credit_spreads"] = (0, "🟡", f"HY spreads {hy:.0f} bps — moderately elevated")

    # Real rates
    rr = signals["real_rate"]
    if rr < MS["real_rate"]["green"]:
        scores["real_rates"] = (1, "🟢", f"Real rate {rr:.3f}% — accommodative")
    elif rr > MS["real_rate"]["red"]:
        scores["real_rates"] = (-1, "🔴", f"Real rate {rr:.3f}% — restrictive territory")
    else:
        scores["real_rates"] = (0, "🟡", f"Real rate {rr:.3f}% — moderate")

    # Breakevens
    be = signals["breakeven_10yr"]
    if MS["breakeven_10yr"]["green_min"] <= be <= MS["breakeven_10yr"]["green_max"]:
        scores["breakevens"] = (1, "🟢", f"10yr breakeven {be:.3f}% — anchored near Fed target")
    elif be > MS["breakeven_10yr"]["red_high"] or be < MS["breakeven_10yr"]["red_low"]:
        scores["breakevens"] = (-1, "🔴", f"10yr breakeven {be:.3f}% — outside normal range")
    else:
        scores["breakevens"] = (0, "🟡", f"10yr breakeven {be:.3f}% — slightly elevated")

    return scores

# ── Determine overall verdict ─────────────────────────────────────
def get_verdict(scores: dict) -> tuple:
    avg = sum(s[0] for s in scores.values()) / len(scores)
    if avg > 0.3:
        return ("🟢 GREEN", "Macro conditions broadly supportive", avg)
    elif avg < -0.3:
        return ("🔴 RED", "Multiple macro concerns active — elevated caution warranted", avg)
    else:
        return ("🟡 YELLOW", "Mixed signals — monitor closely, no clear directional bias", avg)

# ── Print the full brief ──────────────────────────────────────────
def print_macro_brief():
    signals = fetch_all_signals()
    scores  = score_signals(signals)
    verdict, description, avg_score = get_verdict(scores)

    print(f"\n{'🦞 OpenClaw Fixed Income Daily Brief':=^58}")
    print(f"{'Generated: ' + datetime.now().strftime('%Y-%m-%d %H:%M'):^58}\n")

    print("  SIGNAL SCORES")
    print("  " + "─" * 54)
    labels = {
        "yield_curve":    "Yield Curve (2s10s)",
        "credit_spreads": "Credit Spreads (HY)",
        "real_rates":     "Real Rates (10yr)",
        "breakevens":     "Inflation Breakevens",
    }
    for key, label in labels.items():
        val, icon, detail = scores[key]
        print(f"  {icon}  {label:<26} {detail}")

    print(f"\n{'─'*58}")
    print(f"  MACRO VERDICT:  {verdict}")
    print(f"  {description}")
    print(f"  (Composite score: {avg_score:+.2f})")
    print(f"{'':=^58}\n")

print_macro_brief()

This script is the payoff — it pulls fresh data from FRED for all four signals, scores each one, computes an average, and delivers a clear traffic light verdict. It's config-driven, deterministic, and ready to automate.

Sample Output

🦞 OpenClaw Fixed Income Daily Brief
══════════════════════════════════════════════════════════
Generated: 2025-03-14 07:03

  SIGNAL SCORES
  ──────────────────────────────────────────────────────
  🟡  Yield Curve (2s10s)        2s10s at -0.40% — INVERTED
  🟢  Credit Spreads (HY)        HY spreads 385 bps — normal range
  🟢  Real Rates (10yr)          Real rate 1.84% — accommodative
  🟡  Inflation Breakevens       10yr breakeven 2.45% — slightly elevated

  ──────────────────────────────────────────────────────
  MACRO VERDICT:  🟡 YELLOW
  Mixed signals — monitor closely, no clear directional bias
  (Composite score: +0.00)
══════════════════════════════════════════════════════════

In this snapshot, we see a mixed picture: yield curve inverted (concerning), spreads tight (healthy), real rates moderate (accommodative), but breakevens slightly hot. The score averages to zero — Yellow. Watch, but no clear alarm.

Automating the Morning Brief

Schedule the daily brief to run before US markets open with a simple cron entry:

Cron Schedule
# Run at 7am Mon–Fri before US markets open
0 7 * * 1-5 cd /path/to/openclaw && python3 fi_daily_brief.py >> fi_brief.log 2>&1

The script logs its output. Pipe it to email, Slack, or a dashboard. The signal is now part of your morning routine — every trading day, 7am sharp, one unified macro verdict in your inbox.

You've Completed the OpenClaw Fixed Income & Yield Curve Series

  • Part 1 — Yield Curve Monitor: Daily 2s10s spread + inversion alerts
  • Part 2 — Credit Spreads: IG and HY spread monitoring + risk-off alerts
  • Part 3 — Rates Dashboard: Fed Funds, SOFR, real rates morning snapshot
  • Part 4 — Inflation Breakevens: Market's inflation forecast vs actual CPI
  • Part 5 — Macro Signal Generator: All four signals → one daily verdict

All driven by a single fi-config.yaml. One key, one config, five automated briefings.

You now have a production-grade fixed income monitoring suite. Every morning, OpenClaw reads the same signals professional traders watch — yield curves, credit spreads, real rates, inflation expectations — and delivers a single, unified macro call to your inbox. Green, Yellow, or Red. That's the signal. That's the advantage.

Ready to monitor other markets? Return to the OpenClaw Terminal to explore other modules and automation chains.