feat: FXStreet calendar — free upcoming events with forecasts (no API key)
FXStreet calendar.fxstreet.com/eventdate/ returns ~300-400 events over 6 weeks including consensus forecasts, no authentication required. FF HTML scraper is blocked by Cloudflare even on residential IPs. FMP free plan returns 403 on /economic_calendar (requires Starter plan). - Add backend/services/fxstreet_calendar.py: single GET request returning all major currencies; maps Volatility 0/1/2 → low/medium/high - Add POST /api/eco/fxs-sync + GET /api/eco/fxs-sync/status endpoints - Add FXStreet to daily background sync in main.py (runs every 24h) - Add "Sync Upcoming (FXStreet)" button in ImportPanel (no key needed) - Fix FMP 403 error message to say endpoint requires Starter plan - Keep FMP panel for users who upgrade to FMP Starter ($14.99/month) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,11 +133,13 @@ function SurpriseChip({ ev }: { ev: FFEvent }) {
|
||||
// ── Import Panel ──────────────────────────────────────────────────────────────
|
||||
|
||||
function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () => void }) {
|
||||
const [syncResult, setSyncResult] = useState<string | null>(null)
|
||||
const [syncResult, setSyncResult] = useState<string | null>(null)
|
||||
const [fxsMsg, setFxsMsg] = useState<string | null>(null)
|
||||
const [fxsSyncing, setFxsSyncing] = useState(false)
|
||||
const [importStatus, setImportStatus] = useState<ImportStatus>({ running: false, last_result: null })
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const fxsPollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
// Poll import status on mount (auto-import may be running in background)
|
||||
useEffect(() => {
|
||||
const poll = async () => {
|
||||
const r: ImportStatus = await fetch(`${API}/api/eco/ff-import/status`).then(x => x.json()).catch(() => ({ running: false, last_result: null }))
|
||||
@@ -151,7 +153,10 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
}
|
||||
}
|
||||
poll()
|
||||
return () => { if (pollRef.current) clearInterval(pollRef.current) }
|
||||
return () => {
|
||||
if (pollRef.current) clearInterval(pollRef.current)
|
||||
if (fxsPollRef.current) clearInterval(fxsPollRef.current)
|
||||
}
|
||||
}, [onImported])
|
||||
|
||||
const startSync = async () => {
|
||||
@@ -161,12 +166,29 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
await new Promise(res => setTimeout(res, 2500))
|
||||
const s = await fetch(`${API}/api/eco/ff-sync/status`).then(x => x.json())
|
||||
const res = s.last_result || {}
|
||||
const tw = res.thisweek?.events ?? '?'
|
||||
setSyncResult(`✓ live: ${tw} events`)
|
||||
setSyncResult(`✓ live: ${res.thisweek?.events ?? '?'} events`)
|
||||
onImported()
|
||||
} catch { setSyncResult('sync error') }
|
||||
}
|
||||
|
||||
const startFxsSync = async () => {
|
||||
setFxsSyncing(true)
|
||||
setFxsMsg('syncing FXStreet…')
|
||||
try {
|
||||
await fetch(`${API}/api/eco/fxs-sync?weeks=6`, { method: 'POST' })
|
||||
fxsPollRef.current = setInterval(async () => {
|
||||
const s = await fetch(`${API}/api/eco/fxs-sync/status`).then(x => x.json())
|
||||
if (!s.running) {
|
||||
clearInterval(fxsPollRef.current!)
|
||||
setFxsSyncing(false)
|
||||
const r = s.last_result || {}
|
||||
if (r.error) { setFxsMsg(`Error: ${r.error}`); return }
|
||||
setFxsMsg(`✓ FXStreet: ${r.total_upserted} events (${r.date_from} → ${r.date_to})`)
|
||||
onImported()
|
||||
}
|
||||
}, 2000)
|
||||
} catch { setFxsSyncing(false); setFxsMsg('FXStreet error') }
|
||||
}
|
||||
|
||||
const hasData = stats.total > 0
|
||||
const res = importStatus.last_result
|
||||
@@ -185,14 +207,25 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live sync — this week from faireconomy.media JSON */}
|
||||
{/* FF live sync — this week actuals */}
|
||||
<button
|
||||
onClick={startSync}
|
||||
className="px-3 py-1.5 rounded bg-slate-600 hover:bg-slate-500 flex items-center gap-1.5 text-white"
|
||||
title="Update this week actuals from faireconomy.media (Forex Factory JSON)"
|
||||
>
|
||||
<RefreshCw size={13} />
|
||||
Sync Live
|
||||
Sync Live (FF)
|
||||
</button>
|
||||
|
||||
{/* FXStreet — upcoming 6 weeks + forecasts, no API key */}
|
||||
<button
|
||||
onClick={startFxsSync}
|
||||
disabled={fxsSyncing}
|
||||
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="Fetch upcoming 6 weeks with consensus forecasts from FXStreet (no API key required)"
|
||||
>
|
||||
<RefreshCw size={13} className={clsx(fxsSyncing && 'animate-spin')} />
|
||||
Sync Upcoming (FXStreet)
|
||||
</button>
|
||||
|
||||
{res && !importStatus.running && (
|
||||
@@ -201,6 +234,11 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
: <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>}
|
||||
{fxsMsg && (
|
||||
<span className={clsx('text-xs', fxsMsg.startsWith('✓') ? 'text-emerald-400' : fxsMsg.startsWith('Error') ? 'text-red-400' : 'text-yellow-400')}>
|
||||
{fxsMsg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user