feat: import Trading Economics HTML calendar — 993 events with forecasts
Adds a one-click upload+parse flow for the TE calendar page HTML. The TE page (tradingeconomics.com/calendar) contains ~1000 events with actuals, previous, and analyst consensus (forecast) values. - Add backend/services/te_html_parser.py: - parse_html(html): extracts events from <tr data-event> rows - Maps 14 countries to major currencies (all Eurozone → EUR) - Impact inferred from data-category (high/medium/low) - Times converted from Europe/Zurich (CET/CEST) → UTC via zoneinfo - Actual=td[5], Forecast=td[7] (analyst consensus), Previous=td[6] - Add POST /api/eco/te-html-upload (saves file to /tmp) - Add POST /api/eco/te-html-import + GET /status (background parse) - Add "Import TE HTML" upload button in CalendarPage ImportPanel Tested locally: 993 events parsed, 854 with forecast, 861 with actual, date range 2025-03-31 → 2026-06-17, timezone conversion verified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef, KeyboardEvent } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef, KeyboardEvent, useId } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { RefreshCw, ChevronDown, AlertTriangle, Check, Key } from 'lucide-react'
|
||||
import { RefreshCw, ChevronDown, AlertTriangle, Check, Key, Upload } from 'lucide-react'
|
||||
|
||||
const API = ''
|
||||
|
||||
@@ -190,6 +190,38 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
} catch { setFxsSyncing(false); setFxsMsg('FXStreet error') }
|
||||
}
|
||||
|
||||
// TE HTML upload + import
|
||||
const [teHtmlMsg, setTeHtmlMsg] = useState<string | null>(null)
|
||||
const [teHtmlBusy, setTeHtmlBusy] = useState(false)
|
||||
const teHtmlPollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const fileInputId = useId()
|
||||
|
||||
const uploadAndImportTeHtml = async (file: File) => {
|
||||
setTeHtmlBusy(true)
|
||||
setTeHtmlMsg('uploading…')
|
||||
try {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const up = await fetch(`${API}/api/eco/te-html-upload`, { method: 'POST', body: form }).then(x => x.json())
|
||||
if (up.error) { setTeHtmlMsg(`Upload error: ${up.error}`); setTeHtmlBusy(false); return }
|
||||
setTeHtmlMsg(`uploaded (${up.size_mb} MB) — importing…`)
|
||||
await fetch(`${API}/api/eco/te-html-import`, { method: 'POST' })
|
||||
teHtmlPollRef.current = setInterval(async () => {
|
||||
const s = await fetch(`${API}/api/eco/te-html-import/status`).then(x => x.json())
|
||||
if (!s.running) {
|
||||
clearInterval(teHtmlPollRef.current!)
|
||||
setTeHtmlBusy(false)
|
||||
const r = s.last_result || {}
|
||||
if (r.error) { setTeHtmlMsg(`Error: ${r.error}`); return }
|
||||
setTeHtmlMsg(`✓ TE HTML: ${r.total} events (${r.date_from} → ${r.date_to})`)
|
||||
onImported()
|
||||
}
|
||||
}, 2000)
|
||||
} catch { setTeHtmlBusy(false); setTeHtmlMsg('error') }
|
||||
}
|
||||
|
||||
useEffect(() => () => { if (teHtmlPollRef.current) clearInterval(teHtmlPollRef.current) }, [])
|
||||
|
||||
const hasData = stats.total > 0
|
||||
const res = importStatus.last_result
|
||||
|
||||
@@ -228,6 +260,29 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
Sync Upcoming (FXStreet)
|
||||
</button>
|
||||
|
||||
{/* TE HTML import — paste calendar_data.txt from tradingeconomics.com */}
|
||||
<label
|
||||
htmlFor={fileInputId}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded flex items-center gap-1.5 text-white cursor-pointer',
|
||||
teHtmlBusy
|
||||
? 'bg-amber-800 opacity-50 cursor-not-allowed'
|
||||
: 'bg-amber-700 hover:bg-amber-600'
|
||||
)}
|
||||
title="Import Trading Economics HTML page (calendar_data.txt) — 1000 events with forecasts"
|
||||
>
|
||||
<Upload size={13} />
|
||||
Import TE HTML
|
||||
</label>
|
||||
<input
|
||||
id={fileInputId}
|
||||
type="file"
|
||||
accept=".txt,.html"
|
||||
className="hidden"
|
||||
disabled={teHtmlBusy}
|
||||
onChange={e => { const f = e.target.files?.[0]; if (f) uploadAndImportTeHtml(f); e.target.value = '' }}
|
||||
/>
|
||||
|
||||
{res && !importStatus.running && (
|
||||
res.error
|
||||
? <span className="text-red-400 text-xs">Import error: {res.error}</span>
|
||||
@@ -239,6 +294,11 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
{fxsMsg}
|
||||
</span>
|
||||
)}
|
||||
{teHtmlMsg && (
|
||||
<span className={clsx('text-xs', teHtmlMsg.startsWith('✓') ? 'text-emerald-400' : teHtmlMsg.startsWith('Error') ? 'text-red-400' : 'text-yellow-400')}>
|
||||
{teHtmlMsg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user