From 59fbb9b0237510c580dbff41cb883d371811d875 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Tue, 21 Jul 2026 14:08:56 +0200 Subject: [PATCH] feat: calendar eco --- backend/routers/config.py | 4 +++ backend/services/calendar_sync.py | 48 ++++++++++++++++++++--------- backend/services/database.py | 2 +- frontend/src/pages/CalendarPage.tsx | 10 +++++- frontend/src/pages/Config.tsx | 9 ++++-- 5 files changed, 53 insertions(+), 20 deletions(-) diff --git a/backend/routers/config.py b/backend/routers/config.py index 8d70805..588a625 100644 --- a/backend/routers/config.py +++ b/backend/routers/config.py @@ -13,6 +13,7 @@ class ApiKeysRequest(BaseModel): eia_api_key: Optional[str] = None fred_api_key: Optional[str] = None fmp_api_key: Optional[str] = None + te_api_key: Optional[str] = None calendar_refresh_h: Optional[str] = None @@ -66,6 +67,9 @@ def update_api_keys(req: ApiKeysRequest): if req.fmp_api_key is not None: set_config("fmp_api_key", req.fmp_api_key) updated.append("fmp") + if req.te_api_key is not None: + set_config("te_api_key", req.te_api_key) + updated.append("te") if req.calendar_refresh_h is not None: set_config("calendar_refresh_h", req.calendar_refresh_h) updated.append("calendar_refresh_h") diff --git a/backend/services/calendar_sync.py b/backend/services/calendar_sync.py index 43f1617..730ea50 100644 --- a/backend/services/calendar_sync.py +++ b/backend/services/calendar_sync.py @@ -2,8 +2,11 @@ Unified calendar sync. 1. FF live JSON (nfs.faireconomy.media) → this week + next week (actual values, precise) 2. FF HTML scraper → weeks 3..N ahead (upcoming forecasts) - 3. FMP API fallback → weeks 3..N ahead, only if step 2 got blocked (ForexFactory now - 403s the scraper — likely Cloudflare) AND an FMP key is configured (services.fmp_calendar) + 3. Fallback for weeks 3..N, only if step 2 got blocked (ForexFactory now 403s the + scraper — likely Cloudflare): Trading Economics first (services.te_calendar, free + tier, no card required), then FMP (services.fmp_calendar, free tier doesn't include + the economic_calendar endpoint — needs their paid Starter plan) — whichever has a + configured key and actually returns events. Called by the background loop in main.py and manually via POST /api/eco/calendar-sync. """ import logging @@ -52,22 +55,37 @@ def calendar_sync(weeks_ahead: int = 8) -> Dict[str, Any]: print(f"[calendar_sync] scrape result: {r_scrape}", flush=True) # FF's scraper gets 403'd (Cloudflare) often enough that "weeks 3..N" can - # silently come back empty — fall back to FMP for that same range when it does. + # silently come back empty — fall back to Trading Economics (free tier), then + # FMP (needs their paid plan for this endpoint), whichever is configured. if not r_scrape.get("total_upserted"): - print("[calendar_sync] step 3/3 — scrape got 0 events, checking FMP fallback", flush=True) - from services.fmp_calendar import check_fmp_key, fetch_upcoming - key_status = check_fmp_key() - print(f"[calendar_sync] FMP key configured: {key_status['configured']}", flush=True) - if key_status["configured"]: - r_fmp = fetch_upcoming(weeks_ahead=weeks_ahead) - result["fmp_fallback"] = r_fmp - print(f"[calendar_sync] FMP fallback result: {r_fmp}", flush=True) - if r_fmp.get("total_upserted"): - source = "ff+fmp" + print("[calendar_sync] step 3/3 — scrape got 0 events, checking TE/FMP fallback", flush=True) + + from services.te_calendar import check_te_key, fetch_upcoming as te_fetch_upcoming + te_status = check_te_key() + print(f"[calendar_sync] TE key configured: {te_status['configured']}", flush=True) + if te_status["configured"]: + r_te = te_fetch_upcoming(weeks_ahead=weeks_ahead) + result["te_fallback"] = r_te + print(f"[calendar_sync] TE fallback result: {r_te}", flush=True) + if r_te.get("total_upserted"): + source = "ff+te" else: - result["fmp_fallback"] = {"skipped": "no_key"} + result["te_fallback"] = {"skipped": "no_key"} + + if not result["te_fallback"].get("total_upserted"): + from services.fmp_calendar import check_fmp_key, fetch_upcoming as fmp_fetch_upcoming + fmp_status = check_fmp_key() + print(f"[calendar_sync] FMP key configured: {fmp_status['configured']}", flush=True) + if fmp_status["configured"]: + r_fmp = fmp_fetch_upcoming(weeks_ahead=weeks_ahead) + result["fmp_fallback"] = r_fmp + print(f"[calendar_sync] FMP fallback result: {r_fmp}", flush=True) + if r_fmp.get("total_upserted"): + source = "ff+fmp" + else: + result["fmp_fallback"] = {"skipped": "no_key"} else: - print("[calendar_sync] step 3/3 — scrape got events, FMP fallback not needed", flush=True) + print("[calendar_sync] step 3/3 — scrape got events, TE/FMP fallback not needed", flush=True) else: result["scrape"] = {"skipped": True} diff --git a/backend/services/database.py b/backend/services/database.py index 4d129dc..992f5dd 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -1466,7 +1466,7 @@ def set_config(key: str, value: str): os.environ["OPENAI_API_KEY"] = value -_MASKED_CONFIG_KEYS = ["openai_api_key", "newsapi_key", "eia_api_key", "fred_api_key", "fmp_api_key"] +_MASKED_CONFIG_KEYS = ["openai_api_key", "newsapi_key", "eia_api_key", "fred_api_key", "fmp_api_key", "te_api_key"] def get_all_config() -> Dict[str, str]: diff --git a/frontend/src/pages/CalendarPage.tsx b/frontend/src/pages/CalendarPage.tsx index c0cad7d..09284d9 100644 --- a/frontend/src/pages/CalendarPage.tsx +++ b/frontend/src/pages/CalendarPage.tsx @@ -218,6 +218,11 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () => const nw = nwStatus === 'ok' ? r.live.nextweek.events : (r.live?.nextweek?.error ? `err` : (nwStatus ?? '?')) const sc = r.scrape?.total_upserted ?? 0 let msg = `✓ FF: ${tw}+${nw} live, ${sc} upcoming` + if (r.te_fallback) { + if (r.te_fallback.skipped === 'no_key') msg += ' · TE fallback skipped (no key)' + else if (r.te_fallback.error) msg += ` · TE error: ${r.te_fallback.error}` + else msg += ` · TE +${r.te_fallback.total_upserted ?? 0}` + } if (r.fmp_fallback) { if (r.fmp_fallback.skipped === 'no_key') msg += ' · FMP fallback skipped (no key)' else if (r.fmp_fallback.error) msg += ` · FMP error: ${r.fmp_fallback.error}` @@ -259,7 +264,10 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () => const hasData = stats.total > 0 const res = importStatus.last_result - const srcLabel = calStatus.source === 'ff' ? 'ForexFactory' : calStatus.source === 'ff+fmp' ? 'ForexFactory+FMP' : calStatus.source ?? null + const srcLabel = calStatus.source === 'ff' ? 'ForexFactory' + : calStatus.source === 'ff+te' ? 'ForexFactory+TradingEconomics' + : calStatus.source === 'ff+fmp' ? 'ForexFactory+FMP' + : calStatus.source ?? null return (
diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 497e2a5..1f33814 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -799,6 +799,7 @@ export default function Config() { const [eiaKey, setEiaKey] = useState('') const [fredKey, setFredKey] = useState('') const [fmpKey, setFmpKey] = useState('') + const [teKey, setTeKey] = useState('') const [calendarRefreshH, setCalendarRefreshH] = useState(6) const [showKeys, setShowKeys] = useState(false) const [savedMsg, setSavedMsg] = useState('') @@ -962,11 +963,12 @@ export default function Config() { if (eiaKey) keys.eia_api_key = eiaKey if (fredKey) keys.fred_api_key = fredKey if (fmpKey) keys.fmp_api_key = fmpKey + if (teKey) keys.te_api_key = teKey updateApiKeys(keys, { onSuccess: () => { setSavedMsg('API keys saved') setTimeout(() => setSavedMsg(''), 2000) - setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey(''); setFmpKey('') + setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey(''); setFmpKey(''); setTeKey('') } }) } @@ -1644,7 +1646,8 @@ export default function Config() { { label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Get at newsapi.org', configKey: 'newsapi_key' }, { label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Get at eia.gov', configKey: 'eia_api_key' }, { label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Get at fred.stlouisfed.org', configKey: 'fred_api_key' }, - { label: 'FMP API Key', value: fmpKey, setter: setFmpKey, placeholder: 'Calendar fallback — financialmodelingprep.com', configKey: 'fmp_api_key' }, + { label: 'Trading Economics Key', value: teKey, setter: setTeKey, placeholder: 'Calendar fallback (free) — tradingeconomics.com/api/login', configKey: 'te_api_key' }, + { label: 'FMP API Key', value: fmpKey, setter: setFmpKey, placeholder: 'Calendar fallback (paid tier) — financialmodelingprep.com', configKey: 'fmp_api_key' }, ].map(({ label, value, setter, placeholder, configKey }) => (