feat: calendar eco
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user