mirror of
https://github.com/tvishia29-alt/FXandRatesDashboard.git
synced 2026-08-21 11:38:05 +00:00
apis
This commit is contained in:
+112
-93
@@ -142,9 +142,10 @@ FX_PAIRS = {
|
||||
def fetch_fred(series_id, limit=5):
|
||||
"""Fetch latest value from FRED API."""
|
||||
if not FRED_KEY:
|
||||
print(f" FRED key missing for {series_id}")
|
||||
return None
|
||||
try:
|
||||
url = f"https://api.stlouisfed.org/fred/series/observations"
|
||||
url = "https://api.stlouisfed.org/fred/series/observations"
|
||||
params = {
|
||||
"series_id": series_id,
|
||||
"api_key": FRED_KEY,
|
||||
@@ -153,10 +154,12 @@ def fetch_fred(series_id, limit=5):
|
||||
"limit": limit,
|
||||
}
|
||||
r = requests.get(url, params=params, timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
obs = [o for o in data.get("observations", []) if o["value"] != "."]
|
||||
if obs:
|
||||
return float(obs[0]["value"])
|
||||
print(f" No valid FRED obs for {series_id}")
|
||||
except Exception as e:
|
||||
print(f" FRED error ({series_id}): {e}")
|
||||
return None
|
||||
@@ -293,111 +296,84 @@ def fetch_finnhub_calendar():
|
||||
|
||||
|
||||
def fetch_finviz_calendar():
|
||||
"""Scrape economic calendar from Finviz for richer data with beat/miss info."""
|
||||
"""Fetch economic calendar from Finviz — data is embedded as JSON in a script tag."""
|
||||
try:
|
||||
from html.parser import HTMLParser
|
||||
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"}
|
||||
r = requests.get(url, timeout=15, headers=headers)
|
||||
if r.status_code != 200:
|
||||
print(f" Finviz calendar HTTP {r.status_code}")
|
||||
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",
|
||||
}
|
||||
|
||||
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")
|
||||
return []
|
||||
|
||||
class CalParser(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.in_table = False
|
||||
self.in_row = False
|
||||
self.in_cell = False
|
||||
self.cells = []
|
||||
self.current_cell = ""
|
||||
self.rows = []
|
||||
self.table_depth = 0
|
||||
self.target_table = False
|
||||
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)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attrs_d = dict(attrs)
|
||||
if tag == "table" and attrs_d.get("class", "") == "calendar_table":
|
||||
self.target_table = True
|
||||
self.table_depth = 0
|
||||
if self.target_table and tag == "table":
|
||||
self.table_depth += 1
|
||||
if self.target_table and tag == "tr":
|
||||
self.in_row = True
|
||||
self.cells = []
|
||||
if self.target_table and self.in_row and tag == "td":
|
||||
self.in_cell = True
|
||||
self.current_cell = ""
|
||||
date_str = dt[:10] if dt else ""
|
||||
time_str = dt[11:16] if len(dt) > 15 else ""
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if self.target_table and tag == "td" and self.in_cell:
|
||||
self.in_cell = False
|
||||
self.cells.append(self.current_cell.strip())
|
||||
if self.target_table and tag == "tr" and self.in_row:
|
||||
self.in_row = False
|
||||
if len(self.cells) >= 6:
|
||||
self.rows.append(self.cells[:])
|
||||
if self.target_table and tag == "table":
|
||||
self.table_depth -= 1
|
||||
if self.table_depth <= 0:
|
||||
self.target_table = False
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.in_cell:
|
||||
self.current_cell += data
|
||||
|
||||
parser = CalParser()
|
||||
parser.feed(r.text)
|
||||
|
||||
events = []
|
||||
current_date = ""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
for row in parser.rows:
|
||||
# Finviz columns: Date, Time, Release, For, Actual, Expected, Prior
|
||||
date_str = row[0].strip() if row[0].strip() else current_date
|
||||
if date_str:
|
||||
current_date = date_str
|
||||
time_str = row[1].strip() if len(row) > 1 else ""
|
||||
release = row[2].strip() if len(row) > 2 else ""
|
||||
period = row[3].strip() if len(row) > 3 else ""
|
||||
actual = row[4].strip() if len(row) > 4 else ""
|
||||
expected = row[5].strip() if len(row) > 5 else ""
|
||||
prior = row[6].strip() if len(row) > 6 else ""
|
||||
|
||||
if not release:
|
||||
continue
|
||||
|
||||
# Determine beat/miss
|
||||
# Determine beat/miss using isHigherPositive to know direction
|
||||
beat = None
|
||||
if actual and expected and actual != "" and expected != "":
|
||||
if actual is not None and forecast is not None:
|
||||
try:
|
||||
a_val = float(actual.replace("%", "").replace(",", ""))
|
||||
e_val = float(expected.replace("%", "").replace(",", ""))
|
||||
if a_val > e_val:
|
||||
beat = "beat"
|
||||
elif a_val < e_val:
|
||||
beat = "miss"
|
||||
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")
|
||||
else:
|
||||
beat = "inline"
|
||||
except ValueError:
|
||||
beat = "beat" if a_val < f_val else ("miss" if a_val > f_val else "inline")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
events.append({
|
||||
"date": current_date,
|
||||
imp = "red" if importance >= 3 else ("orange" if importance >= 2 else "orange")
|
||||
|
||||
rows.append({
|
||||
"date": date_str,
|
||||
"time": time_str,
|
||||
"ctry": "US",
|
||||
"ev": f"{release}" + (f" ({period})" if period else ""),
|
||||
"prev": prior if prior else "\u2014",
|
||||
"fcast": expected if expected else "\u2014",
|
||||
"act": actual if actual else "\u2014",
|
||||
"imp": "red",
|
||||
"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,
|
||||
"beat": beat,
|
||||
"source": "finviz",
|
||||
})
|
||||
|
||||
print(f" Finviz: {len(events)} calendar events scraped")
|
||||
return events
|
||||
print(f" Finviz parsed rows: {len(rows)}")
|
||||
return rows[:60]
|
||||
|
||||
except Exception as e:
|
||||
print(f" Finviz calendar error: {e}")
|
||||
return []
|
||||
@@ -533,6 +509,35 @@ def main():
|
||||
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"]
|
||||
# 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")
|
||||
output["yields"]["US"] = us_out
|
||||
output["yields"]["US_yf"] = yf_yields
|
||||
|
||||
@@ -608,14 +613,28 @@ def main():
|
||||
print("\n[10] Fetching calendar...")
|
||||
finnhub_cal = fetch_finnhub_calendar()
|
||||
finviz_cal = fetch_finviz_calendar()
|
||||
print(f" Finviz rows: {len(finviz_cal)}")
|
||||
print(f" Finnhub rows: {len(finnhub_cal)}")
|
||||
# Merge: prefer Finviz for US events (has beat/miss), keep Finnhub for non-US
|
||||
if finviz_cal:
|
||||
# Use Finviz as primary, add non-US Finnhub events
|
||||
non_us = [e for e in finnhub_cal if e.get("ctry", "US") != "US"]
|
||||
output["calendar"] = finviz_cal + non_us
|
||||
else:
|
||||
elif finnhub_cal:
|
||||
output["calendar"] = finnhub_cal
|
||||
print(f" {len(output['calendar'])} total events")
|
||||
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'])}")
|
||||
|
||||
# Write output
|
||||
output_path = Path(OUTPUT_FILE)
|
||||
|
||||
Reference in New Issue
Block a user