feat: calendar eco
This commit is contained in:
@@ -13,6 +13,7 @@ class ApiKeysRequest(BaseModel):
|
|||||||
eia_api_key: Optional[str] = None
|
eia_api_key: Optional[str] = None
|
||||||
fred_api_key: Optional[str] = None
|
fred_api_key: Optional[str] = None
|
||||||
fmp_api_key: Optional[str] = None
|
fmp_api_key: Optional[str] = None
|
||||||
|
te_api_key: Optional[str] = None
|
||||||
calendar_refresh_h: 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:
|
if req.fmp_api_key is not None:
|
||||||
set_config("fmp_api_key", req.fmp_api_key)
|
set_config("fmp_api_key", req.fmp_api_key)
|
||||||
updated.append("fmp")
|
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:
|
if req.calendar_refresh_h is not None:
|
||||||
set_config("calendar_refresh_h", req.calendar_refresh_h)
|
set_config("calendar_refresh_h", req.calendar_refresh_h)
|
||||||
updated.append("calendar_refresh_h")
|
updated.append("calendar_refresh_h")
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
Unified calendar sync.
|
Unified calendar sync.
|
||||||
1. FF live JSON (nfs.faireconomy.media) → this week + next week (actual values, precise)
|
1. FF live JSON (nfs.faireconomy.media) → this week + next week (actual values, precise)
|
||||||
2. FF HTML scraper → weeks 3..N ahead (upcoming forecasts)
|
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
|
3. Fallback for weeks 3..N, only if step 2 got blocked (ForexFactory now 403s the
|
||||||
403s the scraper — likely Cloudflare) AND an FMP key is configured (services.fmp_calendar)
|
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.
|
Called by the background loop in main.py and manually via POST /api/eco/calendar-sync.
|
||||||
"""
|
"""
|
||||||
import logging
|
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)
|
print(f"[calendar_sync] scrape result: {r_scrape}", flush=True)
|
||||||
|
|
||||||
# FF's scraper gets 403'd (Cloudflare) often enough that "weeks 3..N" can
|
# 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"):
|
if not r_scrape.get("total_upserted"):
|
||||||
print("[calendar_sync] step 3/3 — scrape got 0 events, checking FMP fallback", flush=True)
|
print("[calendar_sync] step 3/3 — scrape got 0 events, checking TE/FMP fallback", flush=True)
|
||||||
from services.fmp_calendar import check_fmp_key, fetch_upcoming
|
|
||||||
key_status = check_fmp_key()
|
from services.te_calendar import check_te_key, fetch_upcoming as te_fetch_upcoming
|
||||||
print(f"[calendar_sync] FMP key configured: {key_status['configured']}", flush=True)
|
te_status = check_te_key()
|
||||||
if key_status["configured"]:
|
print(f"[calendar_sync] TE key configured: {te_status['configured']}", flush=True)
|
||||||
r_fmp = fetch_upcoming(weeks_ahead=weeks_ahead)
|
if te_status["configured"]:
|
||||||
result["fmp_fallback"] = r_fmp
|
r_te = te_fetch_upcoming(weeks_ahead=weeks_ahead)
|
||||||
print(f"[calendar_sync] FMP fallback result: {r_fmp}", flush=True)
|
result["te_fallback"] = r_te
|
||||||
if r_fmp.get("total_upserted"):
|
print(f"[calendar_sync] TE fallback result: {r_te}", flush=True)
|
||||||
source = "ff+fmp"
|
if r_te.get("total_upserted"):
|
||||||
|
source = "ff+te"
|
||||||
else:
|
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:
|
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:
|
else:
|
||||||
result["scrape"] = {"skipped": True}
|
result["scrape"] = {"skipped": True}
|
||||||
|
|
||||||
|
|||||||
@@ -1466,7 +1466,7 @@ def set_config(key: str, value: str):
|
|||||||
os.environ["OPENAI_API_KEY"] = value
|
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]:
|
def get_all_config() -> Dict[str, str]:
|
||||||
|
|||||||
@@ -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 nw = nwStatus === 'ok' ? r.live.nextweek.events : (r.live?.nextweek?.error ? `err` : (nwStatus ?? '?'))
|
||||||
const sc = r.scrape?.total_upserted ?? 0
|
const sc = r.scrape?.total_upserted ?? 0
|
||||||
let msg = `✓ FF: ${tw}+${nw} live, ${sc} upcoming`
|
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) {
|
||||||
if (r.fmp_fallback.skipped === 'no_key') msg += ' · FMP fallback skipped (no key)'
|
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}`
|
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 hasData = stats.total > 0
|
||||||
const res = importStatus.last_result
|
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 (
|
return (
|
||||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-4 mb-4 flex flex-wrap items-center gap-4 text-sm">
|
<div className="bg-slate-800 border border-slate-700 rounded-lg p-4 mb-4 flex flex-wrap items-center gap-4 text-sm">
|
||||||
|
|||||||
@@ -799,6 +799,7 @@ export default function Config() {
|
|||||||
const [eiaKey, setEiaKey] = useState('')
|
const [eiaKey, setEiaKey] = useState('')
|
||||||
const [fredKey, setFredKey] = useState('')
|
const [fredKey, setFredKey] = useState('')
|
||||||
const [fmpKey, setFmpKey] = useState('')
|
const [fmpKey, setFmpKey] = useState('')
|
||||||
|
const [teKey, setTeKey] = useState('')
|
||||||
const [calendarRefreshH, setCalendarRefreshH] = useState(6)
|
const [calendarRefreshH, setCalendarRefreshH] = useState(6)
|
||||||
const [showKeys, setShowKeys] = useState(false)
|
const [showKeys, setShowKeys] = useState(false)
|
||||||
const [savedMsg, setSavedMsg] = useState('')
|
const [savedMsg, setSavedMsg] = useState('')
|
||||||
@@ -962,11 +963,12 @@ export default function Config() {
|
|||||||
if (eiaKey) keys.eia_api_key = eiaKey
|
if (eiaKey) keys.eia_api_key = eiaKey
|
||||||
if (fredKey) keys.fred_api_key = fredKey
|
if (fredKey) keys.fred_api_key = fredKey
|
||||||
if (fmpKey) keys.fmp_api_key = fmpKey
|
if (fmpKey) keys.fmp_api_key = fmpKey
|
||||||
|
if (teKey) keys.te_api_key = teKey
|
||||||
updateApiKeys(keys, {
|
updateApiKeys(keys, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSavedMsg('API keys saved')
|
setSavedMsg('API keys saved')
|
||||||
setTimeout(() => setSavedMsg(''), 2000)
|
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: '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: '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: '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 }) => (
|
].map(({ label, value, setter, placeholder, configKey }) => (
|
||||||
<div key={label}>
|
<div key={label}>
|
||||||
<label className="text-xs text-slate-500 mb-1 flex items-center justify-between">
|
<label className="text-xs text-slate-500 mb-1 flex items-center justify-between">
|
||||||
@@ -1667,7 +1670,7 @@ export default function Config() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={saveKeys}
|
onClick={saveKeys}
|
||||||
disabled={savingKeys || (!newsapiKey && !eiaKey && !fredKey && !fmpKey)}
|
disabled={savingKeys || (!newsapiKey && !eiaKey && !fredKey && !fmpKey && !teKey)}
|
||||||
className="mt-3 flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
className="mt-3 flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
{savingKeys ? 'Saving...' : 'Save keys'}
|
{savingKeys ? 'Saving...' : 'Save keys'}
|
||||||
|
|||||||
Reference in New Issue
Block a user