feat: replace Trading Economics with FMP for upcoming calendar forecasts

TE costs $199/month and its Economic Calendar is not in the free tier.
FMP (Financial Modeling Prep) offers a free API key (250 req/day) with
full economic calendar coverage including consensus estimates (forecasts).

- Add backend/services/fmp_calendar.py: fetches upcoming events from
  GET /api/v3/economic_calendar (one request, all countries, date range)
- Replace /api/eco/te-key + te-sync endpoints with fmp-key + fmp-sync
- Update daily background sync in main.py to use fmp_calendar
- Replace TEPanel with FMPPanel in CalendarPage.tsx (link to FMP docs)
- Remove broken Cloudflare-blocked FF HTML scrape button from ImportPanel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 17:42:09 +02:00
parent ec6fcc1e3d
commit 5c86d6e715
4 changed files with 199 additions and 74 deletions

View File

@@ -167,27 +167,6 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
} catch { setSyncResult('sync error') }
}
const [scrapeMsg, setScrapeMsg] = useState<string | null>(null)
const scrapeRef = useRef<ReturnType<typeof setInterval> | null>(null)
const startScrape = async () => {
setScrapeMsg('scraping…')
try {
await fetch(`${API}/api/eco/ff-scrape`, { method: 'POST' })
scrapeRef.current = setInterval(async () => {
const s = await fetch(`${API}/api/eco/ff-scrape/status`).then(x => x.json())
if (!s.running) {
clearInterval(scrapeRef.current!)
const r = s.last_result || {}
if (r.error) { setScrapeMsg(`scrape error: ${r.error}`); return }
setScrapeMsg(`${r.total_upserted ?? '?'} events (${r.weeks_scraped} weeks)`)
onImported()
}
}, 3000)
} catch { setScrapeMsg('scrape error') }
}
useEffect(() => () => { if (scrapeRef.current) clearInterval(scrapeRef.current) }, [])
const hasData = stats.total > 0
const res = importStatus.last_result
@@ -216,28 +195,12 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
Sync Live
</button>
{/* HTML scrape — upcoming 5 weeks with forecasts */}
<button
onClick={startScrape}
disabled={scrapeMsg === 'scraping…'}
className="px-3 py-1.5 rounded bg-indigo-700 hover:bg-indigo-600 disabled:opacity-50 flex items-center gap-1.5 text-white"
title="Scrape forexfactory.com for upcoming 5 weeks with consensus forecasts"
>
<RefreshCw size={13} className={clsx(scrapeMsg === 'scraping…' && 'animate-spin')} />
Scrape Upcoming (5w)
</button>
{res && !importStatus.running && (
res.error
? <span className="text-red-400 text-xs">Import error: {res.error}</span>
: <span className="text-emerald-400 text-xs flex items-center gap-1"><Check size={12}/> {res.inserted?.toLocaleString()} inserted</span>
)}
{syncResult && <span className="text-sky-400 text-xs">{syncResult}</span>}
{scrapeMsg && (
<span className={clsx('text-xs', scrapeMsg.startsWith('✓') ? 'text-emerald-400' : scrapeMsg.includes('error') ? 'text-red-400' : 'text-yellow-400')}>
{scrapeMsg}
</span>
)}
</div>
)
}
@@ -305,9 +268,9 @@ function ImpactFilter({ selected, onChange }: { selected: string[]; onChange: (v
)
}
// ── Trading Economics Panel ───────────────────────────────────────────────────
// ── FMP Panel — upcoming forecasts (free API) ─────────────────────────────────
function TEPanel({ onSynced }: { onSynced: () => void }) {
function FMPPanel({ onSynced }: { onSynced: () => void }) {
const [keyStatus, setKeyStatus] = useState<TEKeyStatus | null>(null)
const [keyInput, setKeyInput] = useState('')
const [saving, setSaving] = useState(false)
@@ -316,7 +279,7 @@ function TEPanel({ onSynced }: { onSynced: () => void }) {
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
fetch(`${API}/api/eco/te-key`).then(r => r.json()).then(setKeyStatus).catch(() => {})
fetch(`${API}/api/eco/fmp-key`).then(r => r.json()).then(setKeyStatus).catch(() => {})
return () => { if (pollRef.current) clearInterval(pollRef.current) }
}, [])
@@ -324,7 +287,7 @@ function TEPanel({ onSynced }: { onSynced: () => void }) {
if (!keyInput.trim()) return
setSaving(true)
try {
const r = await fetch(`${API}/api/eco/te-key?key=${encodeURIComponent(keyInput.trim())}`, { method: 'POST' }).then(x => x.json())
const r = await fetch(`${API}/api/eco/fmp-key?key=${encodeURIComponent(keyInput.trim())}`, { method: 'POST' }).then(x => x.json())
setKeyStatus({ configured: true, preview: r.preview })
setKeyInput('')
} finally { setSaving(false) }
@@ -334,9 +297,9 @@ function TEPanel({ onSynced }: { onSynced: () => void }) {
setSyncing(true)
setSyncMsg('syncing…')
try {
await fetch(`${API}/api/eco/te-sync?weeks=6`, { method: 'POST' })
await fetch(`${API}/api/eco/fmp-sync?weeks=6`, { method: 'POST' })
pollRef.current = setInterval(async () => {
const s = await fetch(`${API}/api/eco/te-sync/status`).then(x => x.json())
const s = await fetch(`${API}/api/eco/fmp-sync/status`).then(x => x.json())
if (!s.running) {
clearInterval(pollRef.current!)
setSyncing(false)
@@ -358,8 +321,8 @@ function TEPanel({ onSynced }: { onSynced: () => void }) {
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<Key size={14} className="text-indigo-400" />
<span className="text-slate-300 font-medium">Trading Economics</span>
<span className="text-xs text-slate-500">upcoming forecasts free API</span>
<span className="text-slate-300 font-medium">FMP Calendar</span>
<span className="text-xs text-slate-500">upcoming forecasts free API (250 req/day)</span>
</div>
{configured ? (
@@ -373,7 +336,7 @@ function TEPanel({ onSynced }: { onSynced: () => void }) {
value={keyInput}
onChange={e => setKeyInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Paste TE API key…"
placeholder="Paste FMP API key…"
className="bg-slate-700 border border-slate-600 rounded px-2 py-1 text-xs text-white w-52 focus:outline-none focus:border-indigo-500"
/>
<button
@@ -384,11 +347,11 @@ function TEPanel({ onSynced }: { onSynced: () => void }) {
{saving ? 'Saving…' : 'Save'}
</button>
<a
href="https://tradingeconomics.com/api/login"
href="https://financialmodelingprep.com/developer/docs"
target="_blank" rel="noreferrer"
className="text-xs text-indigo-400 hover:text-indigo-300 underline"
>
Get free key
Free key (financialmodelingprep.com)
</a>
</div>
)}
@@ -520,8 +483,8 @@ export default function CalendarPage() {
{/* Import Panel */}
<ImportPanel stats={stats} onImported={() => { fetchStats(); fetchEvents() }} />
{/* Trading Economics Panel — upcoming forecasts */}
<TEPanel onSynced={() => { fetchStats(); fetchEvents() }} />
{/* FMP Panel — upcoming forecasts */}
<FMPPanel onSynced={() => { fetchStats(); fetchEvents() }} />
{/* Period Tabs */}
<div className="flex flex-wrap gap-1 mb-3">