Files
FXandRatesDashboard/generate.py
T

805 lines
34 KiB
Python
Raw Normal View History

2026-04-09 19:46:17 +00:00
"""
FX RADAR — Data Generation Pipeline
Fetches global yields, commodities, ETFs, vol data, FX, and news.
Outputs data.json which the HTML dashboard reads.
Run locally: python generate.py
Run via GitHub Actions: automatic on schedule
"""
import json
import os
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
import requests
import yfinance as yf
# ─── CONFIG ──────────────────────────────────────────────────────────────────
FRED_KEY = os.environ.get("FRED_API_KEY", "")
FINNHUB_KEY = os.environ.get("FINNHUB_API_KEY", "")
2026-04-12 19:51:08 +00:00
GOV_KEY = os.environ.get("GOV_API_KEY", "")
2026-04-09 19:46:17 +00:00
OUTPUT_FILE = "data.json"
# ─── YIELD CURVE TICKERS (yfinance) ─────────────────────────────────────────
# These are Yahoo Finance tickers for government bond yields globally
YIELD_TICKERS = {
"US": {
"1M": "^IRX", # 3-month T-bill (closest free proxy)
"2Y": None, # Will use FRED
"5Y": "^FVX",
"10Y": "^TNX",
"30Y": "^TYX",
},
"UK": {
"2Y": None,
"5Y": None,
"10Y": None, # We'll scrape or use proxy
},
"Germany": { # Proxy for Eurozone
"2Y": None,
"5Y": None,
"10Y": None,
},
"Japan": {
"2Y": None,
"10Y": None,
},
}
# FRED series for full US yield curve
FRED_YIELDS = {
"DGS1MO": "1M",
"DGS3MO": "3M",
"DGS6MO": "6M",
"DGS1": "1Y",
"DGS2": "2Y",
"DGS3": "3Y",
"DGS5": "5Y",
"DGS7": "7Y",
"DGS10": "10Y",
"DGS20": "20Y",
"DGS30": "30Y",
}
# FRED series for other countries (where available)
FRED_GLOBAL_YIELDS = {
"Eurozone": {"IRLTLT01DEM156N": "10Y"}, # Germany 10Y (monthly, delayed)
"UK": {"IRLTLT01GBM156N": "10Y"},
"Japan": {"IRLTLT01JPM156N": "10Y"},
"Canada": {"IRLTLT01CAM156N": "10Y"},
"Australia": {"IRLTLT01AUM156N": "10Y"},
}
# Bond ETFs as yield proxies (price moves inversely to yields)
BOND_ETFS = {
"TLT": {"name": "US 20Y+ Treasury", "country": "US", "duration": "long"},
"IEF": {"name": "US 7-10Y Treasury", "country": "US", "duration": "medium"},
"SHY": {"name": "US 1-3Y Treasury", "country": "US", "duration": "short"},
"IGLT.L": {"name": "UK Gilts", "country": "UK", "duration": "mixed"},
"IBGL.L": {"name": "Eurozone Govt Bonds", "country": "Eurozone", "duration": "mixed"},
"JGBS": {"name": "Japan Govt Bonds", "country": "Japan", "duration": "mixed"},
"GOVT": {"name": "US Total Treasury", "country": "US", "duration": "mixed"},
}
2026-04-12 20:13:22 +00:00
# ─── EODHD GOVERNMENT BONDS ─────────────────────────────────────────────────
EODHD_BONDS = {
"US2Y.GBOND": {"key": "us2y", "label": "US 2Y", "country": "US"},
"US10Y.GBOND": {"key": "us10y", "label": "US 10Y", "country": "US"},
"US30Y.GBOND": {"key": "us30y", "label": "US 30Y", "country": "US"},
"UK2Y.GBOND": {"key": "uk2y", "label": "UK 2Y", "country": "UK"},
"UK10Y.GBOND": {"key": "uk10y", "label": "UK 10Y", "country": "UK"},
"UK30Y.GBOND": {"key": "uk30y", "label": "UK 30Y", "country": "UK"},
"DE2Y.GBOND": {"key": "de2y", "label": "Germany 2Y", "country": "Eurozone"},
"DE10Y.GBOND": {"key": "de10y", "label": "Germany 10Y", "country": "Eurozone"},
"DE30Y.GBOND": {"key": "de30y", "label": "Germany 30Y", "country": "Eurozone"},
"JP2Y.GBOND": {"key": "jp2y", "label": "Japan 2Y", "country": "Japan"},
"JP10Y.GBOND": {"key": "jp10y", "label": "Japan 10Y", "country": "Japan"},
"JP30Y.GBOND": {"key": "jp30y", "label": "Japan 30Y", "country": "Japan"},
"IT2Y.GBOND": {"key": "it2y", "label": "Italy 2Y", "country": "Eurozone"},
"IT10Y.GBOND": {"key": "it10y", "label": "Italy 10Y", "country": "Eurozone"},
"FR10Y.GBOND": {"key": "fr10y", "label": "France 10Y", "country": "Eurozone"},
"ES10Y.GBOND": {"key": "es10y", "label": "Spain 10Y", "country": "Eurozone"},
"CA2Y.GBOND": {"key": "ca2y", "label": "Canada 2Y", "country": "Canada"},
"CA10Y.GBOND": {"key": "ca10y", "label": "Canada 10Y", "country": "Canada"},
"AU2Y.GBOND": {"key": "au2y", "label": "Australia 2Y", "country": "Australia"},
"AU10Y.GBOND": {"key": "au10y", "label": "Australia 10Y", "country": "Australia"},
"NZ2Y.GBOND": {"key": "nz2y", "label": "New Zealand 2Y", "country": "New Zealand"},
"NZ10Y.GBOND": {"key": "nz10y", "label": "New Zealand 10Y", "country": "New Zealand"},
"SW10Y.GBOND": {"key": "ch10y", "label": "Switzerland 10Y", "country": "Switzerland"},
"SE10Y.GBOND": {"key": "se10y", "label": "Sweden 10Y", "country": "Sweden"},
"NO10Y.GBOND": {"key": "no10y", "label": "Norway 10Y", "country": "Norway"},
}
2026-04-12 20:48:09 +00:00
# ─── CENTRAL BANK POLICY RATES ───────────────────────────────────────────────
# Updated periodically — these change at most ~8 times/year per central bank
POLICY_RATES = {
"US": 4.50, # Fed Funds upper bound
"Eurozone": 2.65, # ECB deposit facility rate
"UK": 4.50, # BoE Bank Rate
"Japan": 0.50, # BoJ overnight rate
"Switzerland": 0.25, # SNB policy rate
"Canada": 2.75, # BoC overnight rate
"Australia": 3.85, # RBA cash rate
"New Zealand": 3.50, # RBNZ OCR
"Sweden": 2.25, # Riksbank repo rate
"Norway": 4.50, # Norges Bank sight deposit rate
}
# ─── MACRO SNAPSHOT DATA ─────────────────────────────────────────────────────
# Updated periodically — CPI/GDP/unemployment are published monthly/quarterly
MACRO_DATA = {
"US": {"cpi": "2.8%", "cpiP": "3.0%", "gdp": "2.4%", "gdpT": "Q1 2025", "unemp": "4.2%", "unempT": "Jun 2025", "stance": "Hawkish Hold", "last": "Hold 4.50%", "next": "Jul 30", "pricing": "1 cut by Sep"},
"Eurozone": {"cpi": "2.1%", "cpiP": "2.2%", "gdp": "1.0%", "gdpT": "Q1 2025", "unemp": "6.3%", "unempT": "May 2025", "stance": "Dovish Easing", "last": "Cut to 2.65%", "next": "Jul 17", "pricing": "1 more cut 2025"},
"UK": {"cpi": "3.5%", "cpiP": "3.3%", "gdp": "1.3%", "gdpT": "Q1 2025", "unemp": "4.4%", "unempT": "May 2025", "stance": "Cautious Hold", "last": "Hold 4.50%", "next": "Aug 7", "pricing": "1 cut by Nov"},
"Japan": {"cpi": "3.6%", "cpiP": "3.2%", "gdp": "-0.7%", "gdpT": "Q1 2025", "unemp": "2.5%", "unempT": "May 2025", "stance": "Gradual Tightening", "last": "Hold 0.50%", "next": "Jul 31", "pricing": "Hike to 0.75% H2"},
"Switzerland": {"cpi": "0.6%", "cpiP": "0.3%", "gdp": "1.4%", "gdpT": "Q1 2025", "unemp": "2.8%", "unempT": "Jun 2025", "stance": "Neutral", "last": "Cut to 0.25%", "next": "Sep 18", "pricing": "Hold through 2025"},
"Canada": {"cpi": "2.9%", "cpiP": "2.7%", "gdp": "1.7%", "gdpT": "Q1 2025", "unemp": "6.7%", "unempT": "Jun 2025", "stance": "Easing", "last": "Cut to 2.75%", "next": "Jul 30", "pricing": "1 more cut 2025"},
"Australia": {"cpi": "2.4%", "cpiP": "2.8%", "gdp": "1.3%", "gdpT": "Q1 2025", "unemp": "4.0%", "unempT": "May 2025", "stance": "Easing", "last": "Cut to 3.85%", "next": "Aug 5", "pricing": "1-2 cuts by Dec"},
"New Zealand": {"cpi": "2.5%", "cpiP": "2.2%", "gdp": "0.7%", "gdpT": "Q1 2025", "unemp": "5.1%", "unempT": "Q1 2025", "stance": "Easing Cycle","last": "Cut to 3.50%", "next": "Jul 9", "pricing": "2 more cuts 2025"},
"Sweden": {"cpi": "2.3%", "cpiP": "2.5%", "gdp": "1.1%", "gdpT": "Q1 2025", "unemp": "8.9%", "unempT": "May 2025", "stance": "Dovish Hold", "last": "Hold 2.25%", "next": "Aug 20", "pricing": "Hold through 2025"},
"Norway": {"cpi": "2.7%", "cpiP": "2.8%", "gdp": "1.5%", "gdpT": "Q1 2025", "unemp": "4.1%", "unempT": "May 2025", "stance": "Hawkish Hold","last": "Hold 4.50%", "next": "Aug 14", "pricing": "Cut Q4 2025"},
}
2026-04-09 19:46:17 +00:00
# ─── COMMODITY TICKERS ───────────────────────────────────────────────────────
COMMODITIES = {
2026-04-09 20:15:17 +00:00
"GC=F": {"key": "gold", "name": "Gold", "unit": "$/oz"},
"SI=F": {"key": "silver", "name": "Silver", "unit": "$/oz"},
"CL=F": {"key": "wti", "name": "WTI Crude", "unit": "$/bbl"},
"BZ=F": {"key": "brent", "name": "Brent Crude", "unit": "$/bbl"},
"NG=F": {"key": "natgas", "name": "Natural Gas", "unit": "$/MMBtu"},
"HG=F": {"key": "copper", "name": "Copper", "unit": "$/lb"},
"ZW=F": {"key": "wheat", "name": "Wheat", "unit": "¢/bu"},
"ZC=F": {"key": "corn", "name": "Corn", "unit": "¢/bu"},
2026-04-09 19:46:17 +00:00
}
# ─── KEY ETFs ────────────────────────────────────────────────────────────────
KEY_ETFS = {
"SPY": "S&P 500",
"QQQ": "Nasdaq 100",
"IWM": "Russell 2000",
"EEM": "EM Equities",
"FXI": "China Large Cap",
"EWJ": "Japan Equities",
"EWZ": "Brazil Equities",
"EWG": "Germany Equities",
"EWU": "UK Equities",
"UUP": "US Dollar Index",
"FXE": "Euro ETF",
"FXY": "Yen ETF",
"FXB": "GBP ETF",
"FXA": "AUD ETF",
"FXC": "CAD ETF",
"GLD": "Gold ETF",
"USO": "Oil ETF",
"DBA": "Agriculture ETF",
"XLE": "Energy Sector",
"XLF": "Financials Sector",
}
# ─── VOLATILITY ──────────────────────────────────────────────────────────────
VOL_TICKERS = {
"^VIX": "VIX (S&P 500 Vol)",
"^VXN": "VXN (Nasdaq Vol)",
"^MOVE": "MOVE (Bond Vol)",
"VIXY": "VIX Short-Term ETF",
}
# ─── FX PAIRS (yfinance format) ─────────────────────────────────────────────
FX_PAIRS = {
"EURUSD=X": "EURUSD", "GBPUSD=X": "GBPUSD", "USDJPY=X": "USDJPY",
"USDCHF=X": "USDCHF", "AUDUSD=X": "AUDUSD", "NZDUSD=X": "NZDUSD",
"USDCAD=X": "USDCAD", "USDSEK=X": "USDSEK", "USDNOK=X": "USDNOK",
"USDCNY=X": "USDCNY", "USDMXN=X": "USDMXN", "USDBRL=X": "USDBRL",
"USDZAR=X": "USDZAR", "USDINR=X": "USDINR", "USDKRW=X": "USDKRW",
"USDTRY=X": "USDTRY", "USDPLN=X": "USDPLN", "USDHUF=X": "USDHUF",
"USDCZK=X": "USDCZK", "USDSGD=X": "USDSGD", "USDIDR=X": "USDIDR",
"USDTHB=X": "USDTHB", "DX-Y.NYB": "DXY",
}
2026-04-12 20:13:22 +00:00
def fetch_eodhd_eod(symbol):
"""Fetch latest EOD data from EODHD for a bond/rate symbol."""
2026-04-12 19:51:08 +00:00
if not GOV_KEY:
return None
try:
2026-04-12 20:13:22 +00:00
url = f"https://eodhd.com/api/eod/{symbol}"
params = {"api_token": GOV_KEY, "fmt": "json", "order": "d", "limit": 2}
r = requests.get(url, params=params, timeout=15)
2026-04-12 19:51:08 +00:00
r.raise_for_status()
2026-04-12 20:13:22 +00:00
data = r.json()
if not isinstance(data, list) or len(data) < 1:
return None
latest = data[0]
prev = data[1] if len(data) >= 2 else None
value = latest.get("close")
prev_value = prev.get("close") if prev else None
change = round(value - prev_value, 4) if value is not None and prev_value is not None else None
pct_change = (
round((value - prev_value) / prev_value * 100, 4)
if value is not None and prev_value is not None and prev_value != 0
else None
)
return {
"value": value,
"change": change,
"pct_change": pct_change,
"date": latest.get("date"),
}
2026-04-12 19:51:08 +00:00
except Exception as e:
2026-04-12 20:13:22 +00:00
print(f" EODHD error ({symbol}): {e}")
2026-04-12 19:51:08 +00:00
return None
2026-04-09 19:46:17 +00:00
def fetch_fred(series_id, limit=5):
"""Fetch latest value from FRED API."""
if not FRED_KEY:
2026-04-09 21:13:38 +00:00
print(f" FRED key missing for {series_id}")
2026-04-09 19:46:17 +00:00
return None
try:
2026-04-09 21:13:38 +00:00
url = "https://api.stlouisfed.org/fred/series/observations"
2026-04-09 19:46:17 +00:00
params = {
"series_id": series_id,
"api_key": FRED_KEY,
"file_type": "json",
"sort_order": "desc",
"limit": limit,
}
r = requests.get(url, params=params, timeout=10)
2026-04-09 21:13:38 +00:00
r.raise_for_status()
2026-04-09 19:46:17 +00:00
data = r.json()
obs = [o for o in data.get("observations", []) if o["value"] != "."]
if obs:
return float(obs[0]["value"])
2026-04-09 21:13:38 +00:00
print(f" No valid FRED obs for {series_id}")
2026-04-09 19:46:17 +00:00
except Exception as e:
print(f" FRED error ({series_id}): {e}")
return None
def fetch_yf_quotes(tickers_dict, period="5d"):
"""Fetch quotes from Yahoo Finance for a dict of ticker: name."""
results = {}
ticker_list = list(tickers_dict.keys())
try:
# Batch download for efficiency
data = yf.download(ticker_list, period=period, progress=False, threads=True)
for ticker in ticker_list:
name = tickers_dict[ticker]
try:
if len(ticker_list) == 1:
close_series = data["Close"]
else:
close_series = data["Close"][ticker]
closes = close_series.dropna()
if len(closes) >= 1:
last = round(float(closes.iloc[-1]), 4)
prev = round(float(closes.iloc[-2]), 4) if len(closes) >= 2 else None
chg = round((last / prev - 1) * 100, 2) if prev and prev > 0 else None
# Calculate 1W, 1M changes if we have enough data
results[name] = {
"price": last,
"prev_close": prev,
"change_pct": chg,
"timestamp": str(closes.index[-1]),
}
except Exception as e:
print(f" yfinance parse error ({ticker}): {e}")
except Exception as e:
print(f" yfinance batch error: {e}")
return results
def fetch_yf_history(tickers_dict, period="3mo"):
"""Fetch longer history for calculating 1W, 1M, YTD changes."""
results = {}
ticker_list = list(tickers_dict.keys())
try:
data = yf.download(ticker_list, period=period, progress=False, threads=True)
for ticker in ticker_list:
name = tickers_dict[ticker]
try:
if len(ticker_list) == 1:
closes = data["Close"].dropna()
else:
closes = data["Close"][ticker].dropna()
if len(closes) < 2:
continue
last = float(closes.iloc[-1])
d1 = float(closes.iloc[-2]) if len(closes) >= 2 else None
w1 = float(closes.iloc[-6]) if len(closes) >= 6 else None
m1 = float(closes.iloc[-22]) if len(closes) >= 22 else None
results[name] = {
"price": round(last, 4),
"d1_pct": round((last / d1 - 1) * 100, 2) if d1 else None,
"w1_pct": round((last / w1 - 1) * 100, 2) if w1 else None,
"m1_pct": round((last / m1 - 1) * 100, 2) if m1 else None,
}
except Exception as e:
print(f" History parse error ({ticker}): {e}")
except Exception as e:
print(f" History batch error: {e}")
return results
def fetch_finnhub_news(category="forex"):
"""Fetch news from Finnhub."""
if not FINNHUB_KEY:
return []
try:
url = f"https://finnhub.io/api/v1/news?category={category}&token={FINNHUB_KEY}"
r = requests.get(url, timeout=10)
articles = r.json()
return [
{
"headline": a.get("headline", ""),
"source": a.get("source", ""),
"url": a.get("url", ""),
"datetime": a.get("datetime", 0),
"category": category,
}
for a in articles[:30]
]
except Exception as e:
print(f" Finnhub news error: {e}")
return []
def fetch_finnhub_calendar():
"""Fetch economic calendar from Finnhub."""
if not FINNHUB_KEY:
return []
try:
today = datetime.now().strftime("%Y-%m-%d")
future = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
url = f"https://finnhub.io/api/v1/calendar/economic?from={today}&to={future}&token={FINNHUB_KEY}"
r = requests.get(url, timeout=10)
data = r.json()
events = data.get("economicCalendar", [])
return [
{
"date": e.get("time", "")[:10],
"time": e.get("time", "")[11:16],
2026-04-09 20:15:17 +00:00
"ctry": e.get("country", ""),
"ev": e.get("event", ""),
"prev": e.get("prev") if e.get("prev") is not None else "\u2014",
"fcast": e.get("estimate") if e.get("estimate") is not None else "\u2014",
"act": e.get("actual") if e.get("actual") is not None else "\u2014",
"imp": "red" if e.get("impact") == "high" else "orange",
2026-04-09 19:46:17 +00:00
}
for e in events[:40]
]
except Exception as e:
print(f" Finnhub calendar error: {e}")
return []
2026-04-09 20:58:07 +00:00
def fetch_finviz_calendar():
2026-04-09 21:13:38 +00:00
"""Fetch economic calendar from Finviz — data is embedded as JSON in a script tag."""
2026-04-09 20:58:07 +00:00
try:
2026-04-09 21:13:38 +00:00
from bs4 import BeautifulSoup
except ImportError:
print(" bs4 not installed — skipping Finviz calendar")
return []
url = "https://finviz.com/calendar.ashx"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept-Language": "en-GB,en;q=0.9",
}
2026-04-09 20:58:07 +00:00
2026-04-09 21:13:38 +00:00
try:
r = requests.get(url, headers=headers, timeout=20)
r.raise_for_status()
print(f" Finviz status: {r.status_code}, html length: {len(r.text)}")
soup = BeautifulSoup(r.text, "html.parser")
# Calendar data is embedded as JSON in a <script> tag
entries = []
for script in soup.find_all("script"):
txt = script.string or ""
if "entries" in txt and "calendarId" in txt:
data = json.loads(txt)
entries = data.get("data", {}).get("entries", [])
break
if not entries:
print(" Finviz: no JSON entries found in page")
2026-04-09 20:58:07 +00:00
return []
2026-04-09 21:13:38 +00:00
rows = []
for e in entries:
ev_name = e.get("event", "")
ref = e.get("reference", "")
dt = e.get("date", "")
actual = e.get("actual")
forecast = e.get("forecast")
previous = e.get("previous")
importance = e.get("importance", 1)
date_str = dt[:10] if dt else ""
time_str = dt[11:16] if len(dt) > 15 else ""
# Determine beat/miss using isHigherPositive to know direction
2026-04-09 20:58:07 +00:00
beat = None
2026-04-09 21:13:38 +00:00
if actual is not None and forecast is not None:
2026-04-09 20:58:07 +00:00
try:
2026-04-09 21:13:38 +00:00
a_val = float(str(actual).replace("%", "").replace(",", ""))
f_val = float(str(forecast).replace("%", "").replace(",", ""))
higher_good = e.get("isHigherPositive", 1)
if higher_good:
beat = "beat" if a_val > f_val else ("miss" if a_val < f_val else "inline")
2026-04-09 20:58:07 +00:00
else:
2026-04-09 21:13:38 +00:00
beat = "beat" if a_val < f_val else ("miss" if a_val > f_val else "inline")
except (ValueError, TypeError):
2026-04-09 20:58:07 +00:00
pass
2026-04-09 21:13:38 +00:00
imp = "red" if importance >= 3 else ("orange" if importance >= 2 else "orange")
rows.append({
"date": date_str,
2026-04-09 20:58:07 +00:00
"time": time_str,
2026-04-09 21:13:38 +00:00
"ctry": "🇺🇸",
"ev": f"{ev_name}" + (f" ({ref})" if ref else ""),
"prev": str(previous) if previous is not None else "—",
"fcast": str(forecast) if forecast is not None else "—",
"act": str(actual) if actual is not None else "—",
"imp": imp,
2026-04-09 20:58:07 +00:00
"beat": beat,
"source": "finviz",
})
2026-04-09 21:13:38 +00:00
print(f" Finviz parsed rows: {len(rows)}")
return rows[:60]
2026-04-09 20:58:07 +00:00
except Exception as e:
print(f" Finviz calendar error: {e}")
return []
2026-04-09 19:46:17 +00:00
def fetch_rss_news():
"""Fetch news from free RSS feeds (no API key needed)."""
import xml.etree.ElementTree as ET
feeds = [
("https://feeds.content.dowjones.io/public/rss/mw_topstories", "MarketWatch"),
("https://feeds.bbci.co.uk/news/business/rss.xml", "BBC Business"),
]
articles = []
for url, source in feeds:
try:
r = requests.get(url, timeout=10, headers={"User-Agent": "FXRadar/1.0"})
root = ET.fromstring(r.content)
for item in root.findall(".//item")[:10]:
title = item.findtext("title", "")
link = item.findtext("link", "")
pub = item.findtext("pubDate", "")
2026-04-09 20:15:17 +00:00
try:
pub_ts = int(datetime.strptime(pub, "%a, %d %b %Y %H:%M:%S %Z").timestamp())
except Exception:
pub_ts = 0
2026-04-09 19:46:17 +00:00
if title:
articles.append({
"headline": title,
"source": source,
"url": link,
2026-04-09 20:15:17 +00:00
"datetime": pub_ts,
2026-04-09 19:46:17 +00:00
"category": "general",
})
except Exception as e:
print(f" RSS error ({source}): {e}")
return articles
2026-04-09 20:15:17 +00:00
def fetch_single_ticker_history(ticker, name, period="3mo"):
"""Fallback: fetch a single ticker individually if batch download fails."""
try:
data = yf.download(ticker, period=period, progress=False, threads=False)
closes = data["Close"].dropna()
if len(closes) < 2:
return None
last = float(closes.iloc[-1])
d1 = float(closes.iloc[-2]) if len(closes) >= 2 else None
w1 = float(closes.iloc[-6]) if len(closes) >= 6 else None
m1 = float(closes.iloc[-22]) if len(closes) >= 22 else None
return {
"price": round(last, 4),
"d1_pct": round((last / d1 - 1) * 100, 2) if d1 else None,
"w1_pct": round((last / w1 - 1) * 100, 2) if w1 else None,
"m1_pct": round((last / m1 - 1) * 100, 2) if m1 else None,
}
except Exception as e:
print(f" Single ticker history error ({ticker}): {e}")
return None
2026-04-09 19:46:17 +00:00
def main():
print("=" * 60)
print(f"FX RADAR Data Generation — {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
print("=" * 60)
output = {
"generated_at": datetime.utcnow().isoformat() + "Z",
"yields": {},
"commodities": {},
"etfs": {},
"fx": {},
"volatility": {},
"bond_etfs": {},
"news": [],
"calendar": [],
2026-04-12 19:51:08 +00:00
"policy_rates": {},
"macro": {},
"sentiment": {},
"scenarios": {},
2026-04-12 20:13:22 +00:00
"gov_bonds": {},
2026-04-09 19:46:17 +00:00
}
# 1. US YIELD CURVE from FRED (most reliable)
print("\n[1/8] Fetching US yield curve from FRED...")
us_yields = {}
for series_id, tenor in FRED_YIELDS.items():
val = fetch_fred(series_id)
if val is not None:
us_yields[tenor] = val
print(f" US {tenor}: {val}%")
time.sleep(0.2) # Rate limit
2026-04-09 20:15:17 +00:00
output["yields"]["US"] = {
"y1m": us_yields.get("1M"),
"y3m": us_yields.get("3M"),
"y6m": us_yields.get("6M"),
"y1": us_yields.get("1Y"),
"y2": us_yields.get("2Y"),
"y3": us_yields.get("3Y"),
"y5": us_yields.get("5Y"),
"y7": us_yields.get("7Y"),
"y10": us_yields.get("10Y"),
"y20": us_yields.get("20Y"),
"y30": us_yields.get("30Y"),
}
2026-04-09 19:46:17 +00:00
# 2. Global yields from FRED (limited but free)
print("\n[2/8] Fetching global yields from FRED...")
for country, series_map in FRED_GLOBAL_YIELDS.items():
country_yields = {}
for series_id, tenor in series_map.items():
val = fetch_fred(series_id)
if val is not None:
country_yields[tenor] = val
print(f" {country} {tenor}: {val}%")
time.sleep(0.2)
if country_yields:
2026-04-09 20:15:17 +00:00
output["yields"][country] = {
"y10": country_yields.get("10Y"),
"y2": country_yields.get("2Y"),
"y5": country_yields.get("5Y"),
}
2026-04-09 19:46:17 +00:00
# 3. Yield proxies from Yahoo Finance (US treasuries)
print("\n[3/8] Fetching yield data from Yahoo Finance...")
yf_yields = fetch_yf_quotes({
"^IRX": "US_3M", "^FVX": "US_5Y", "^TNX": "US_10Y", "^TYX": "US_30Y"
})
for name, data in yf_yields.items():
print(f" {name}: {data['price']}")
2026-04-09 20:15:17 +00:00
# Fill missing US yields from Yahoo proxies if FRED missing
us_out = output["yields"].get("US", {})
if us_out.get("y3m") is None and yf_yields.get("US_3M"):
us_out["y3m"] = yf_yields["US_3M"]["price"]
if us_out.get("y5") is None and yf_yields.get("US_5Y"):
us_out["y5"] = yf_yields["US_5Y"]["price"]
if us_out.get("y10") is None and yf_yields.get("US_10Y"):
us_out["y10"] = yf_yields["US_10Y"]["price"]
if us_out.get("y30") is None and yf_yields.get("US_30Y"):
us_out["y30"] = yf_yields["US_30Y"]["price"]
2026-04-09 21:13:38 +00:00
# Fallback for 2Y: try fetching via yfinance ticker
if us_out.get("y2") is None:
print(" US 2Y missing from FRED, trying yfinance fallback...")
try:
for sym in ["ZT=F"]: # 2-Year T-Note futures
t2 = yf.download(sym, period="5d", progress=False)
if not t2.empty:
c2 = t2["Close"].dropna()
if len(c2) > 0:
# ZT=F trades as price not yield; approximate yield
price = float(c2.iloc[-1])
# 2Y note: yield ≈ (100 - price) * 2 / 100 roughly, but
# better to interpolate from 3M and 5Y if available
break
except Exception as e:
print(f" US 2Y futures fallback error: {e}")
# If still null, interpolate from 3M and 5Y
if us_out.get("y2") is None:
y3m = us_out.get("y3m")
y5 = us_out.get("y5")
if y3m is not None and y5 is not None:
# Linear interpolation: 2Y is ~36% between 3M and 5Y on the curve
us_out["y2"] = round(y3m + (y5 - y3m) * 0.36, 3)
print(f" US 2Y interpolated from 3M/5Y: {us_out['y2']}%")
elif y5 is not None:
us_out["y2"] = round(y5 - 0.15, 3) # rough estimate
print(f" US 2Y estimated from 5Y: {us_out['y2']}%")
if us_out.get("y2") is None:
print(" ⚠ US 2Y still null — carry/spread logic will be limited")
2026-04-09 20:15:17 +00:00
output["yields"]["US"] = us_out
2026-04-09 19:46:17 +00:00
output["yields"]["US_yf"] = yf_yields
# 4. Commodities
print("\n[4/8] Fetching commodities...")
2026-04-09 20:15:17 +00:00
commod_data_raw = fetch_yf_history({k: v["name"] for k, v in COMMODITIES.items()})
commod_data = {}
print(" Commodity raw keys:", list(commod_data_raw.keys()))
for ticker, info in COMMODITIES.items():
name = info["name"]
key = info["key"]
row = commod_data_raw.get(name)
if row:
row["unit"] = info.get("unit", "")
commod_data[key] = row
print(f" {name}: ${row['price']} ({row.get('d1_pct', '?')}%)")
else:
# Fallback: try single ticker download
single = fetch_single_ticker_history(ticker, name)
if single:
single["unit"] = info.get("unit", "")
commod_data[key] = single
print(f" {name} (fallback): ${single['price']} ({single.get('d1_pct', '?')}%)")
2026-04-09 19:46:17 +00:00
output["commodities"] = commod_data
# 5. Key ETFs
print("\n[5/8] Fetching ETFs...")
etf_data = fetch_yf_history(KEY_ETFS)
for name, data in etf_data.items():
print(f" {name}: ${data['price']} ({data.get('d1_pct', '?')}%)")
output["etfs"] = etf_data
# 6. Bond ETFs
print("\n[6/8] Fetching bond ETFs...")
bond_data = fetch_yf_history({k: v["name"] for k, v in BOND_ETFS.items()})
for name, data in bond_data.items():
info = next((v for k, v in BOND_ETFS.items() if v["name"] == name), {})
data["country"] = info.get("country", "")
data["duration"] = info.get("duration", "")
output["bond_etfs"] = bond_data
# 7. Volatility
print("\n[7/8] Fetching volatility data...")
vol_data = fetch_yf_quotes(VOL_TICKERS)
for name, data in vol_data.items():
print(f" {name}: {data['price']}")
output["volatility"] = vol_data
# 8. FX
print("\n[8/8] Fetching FX rates...")
fx_data = fetch_yf_history(FX_PAIRS)
for name, data in fx_data.items():
print(f" {name}: {data['price']} ({data.get('d1_pct', '?')}%)")
output["fx"] = fx_data
# 9. News (Finnhub + RSS)
print("\n[9] Fetching news...")
finnhub_news = fetch_finnhub_news("forex") + fetch_finnhub_news("general")
rss_news = fetch_rss_news()
all_news = finnhub_news + rss_news
# Deduplicate by headline
seen = set()
unique_news = []
for n in all_news:
key = n["headline"][:50]
if key not in seen:
seen.add(key)
unique_news.append(n)
output["news"] = unique_news[:40]
print(f" {len(unique_news)} unique articles")
2026-04-09 20:58:07 +00:00
# 10. Economic Calendar (Finnhub + Finviz)
2026-04-09 19:46:17 +00:00
print("\n[10] Fetching calendar...")
2026-04-09 20:58:07 +00:00
finnhub_cal = fetch_finnhub_calendar()
finviz_cal = fetch_finviz_calendar()
2026-04-09 21:13:38 +00:00
print(f" Finviz rows: {len(finviz_cal)}")
print(f" Finnhub rows: {len(finnhub_cal)}")
2026-04-09 20:58:07 +00:00
# Merge: prefer Finviz for US events (has beat/miss), keep Finnhub for non-US
if finviz_cal:
non_us = [e for e in finnhub_cal if e.get("ctry", "US") != "US"]
output["calendar"] = finviz_cal + non_us
2026-04-09 21:13:38 +00:00
elif finnhub_cal:
2026-04-09 20:58:07 +00:00
output["calendar"] = finnhub_cal
2026-04-09 21:13:38 +00:00
else:
print(" ⚠ Both calendar sources empty — inserting debug placeholder")
output["calendar"] = [{
"date": datetime.now().strftime("%Y-%m-%d"),
"time": "08:30",
"ctry": "🇺🇸",
"ev": "[Calendar sources unavailable]",
"prev": "—",
"fcast": "—",
"act": "—",
"imp": "orange",
"beat": None,
}]
print(f" Final calendar rows: {len(output['calendar'])}")
2026-04-09 19:46:17 +00:00
2026-04-12 20:48:09 +00:00
# 11. Policy rates & macro data
print("\n[11] Loading policy rates & macro data...")
output["policy_rates"] = POLICY_RATES
output["macro"] = MACRO_DATA
print(f" Policy rates: {len(output['policy_rates'])} countries")
print(f" Macro data: {len(output['macro'])} countries")
# 12. Government bonds (EODHD)
print("\n[12] Fetching government bonds from EODHD...")
2026-04-12 20:13:22 +00:00
if GOV_KEY:
for symbol, info in EODHD_BONDS.items():
result = fetch_eodhd_eod(symbol)
if result:
result["label"] = info["label"]
result["country"] = info["country"]
output["gov_bonds"][info["key"]] = result
v = result["value"]
c = result.get("change")
chg_str = f" ({c:+.3f})" if c is not None else ""
print(f" {info['label']}: {v}%{chg_str}")
time.sleep(0.3) # Rate limit
print(f" Fetched {len(output['gov_bonds'])} bonds")
2026-04-12 20:35:22 +00:00
# Backfill yields from EODHD bonds for the curve/spreads views
bond_to_yield = {
"us2y": ("US", "y2"), "us10y": ("US", "y10"), "us30y": ("US", "y30"),
"uk2y": ("UK", "y2"), "uk10y": ("UK", "y10"), "uk30y": ("UK", "y30"),
"de2y": ("Eurozone", "y2"), "de10y": ("Eurozone", "y10"), "de30y": ("Eurozone", "y30"),
"jp2y": ("Japan", "y2"), "jp10y": ("Japan", "y10"), "jp30y": ("Japan", "y30"),
"it2y": ("Italy", "y2"), "it10y": ("Italy", "y10"),
"fr10y": ("France", "y10"), "es10y": ("Spain", "y10"),
"ca2y": ("Canada", "y2"), "ca10y": ("Canada", "y10"),
"au2y": ("Australia", "y2"), "au10y": ("Australia", "y10"),
"nz2y": ("New Zealand", "y2"), "nz10y": ("New Zealand", "y10"),
"ch10y": ("Switzerland", "y10"), "se10y": ("Sweden", "y10"), "no10y": ("Norway", "y10"),
}
for bond_key, (country, tenor) in bond_to_yield.items():
bond = output["gov_bonds"].get(bond_key)
if bond and bond.get("value") is not None:
if country not in output["yields"]:
output["yields"][country] = {}
# Only fill if not already set from FRED/yfinance
if output["yields"][country].get(tenor) is None:
output["yields"][country][tenor] = bond["value"]
print(" Backfilled yields from EODHD bonds")
2026-04-12 20:13:22 +00:00
else:
print(" EODHD API key not set — skipping government bonds")
2026-04-12 19:51:08 +00:00
2026-04-09 19:46:17 +00:00
# Write output
output_path = Path(OUTPUT_FILE)
with open(output_path, "w") as f:
json.dump(output, f, indent=2, default=str)
size_kb = output_path.stat().st_size / 1024
print(f"\n{'=' * 60}")
print(f"✓ Generated {OUTPUT_FILE} ({size_kb:.1f} KB)")
print(f" Yields: {len(output['yields'])} countries")
print(f" Commodities: {len(output['commodities'])} instruments")
print(f" ETFs: {len(output['etfs'])} funds")
print(f" FX: {len(output['fx'])} pairs")
print(f" News: {len(output['news'])} articles")
print(f" Calendar: {len(output['calendar'])} events")
2026-04-12 20:13:22 +00:00
print(f" Gov bonds: {len(output['gov_bonds'])} instruments")
2026-04-09 19:46:17 +00:00
print(f"{'=' * 60}")
if __name__ == "__main__":
main()