feat: market events db migration + generation date filter
Database migration: - Add 'origin' and 'source_refs' columns to market_events ALTER TABLE migration (sub_type/actual_value/expected_value/surprise_pct were already there) - All new tables (macro_gauge_snapshots, ai_desks) created via CREATE TABLE IF NOT EXISTS on next init_db() call (container restart) Backend: - GET /api/market-events/db-status — health check returning row counts, latest dates, and missing columns for all 6 tables needed by the detector - list_events() now accepts gen_date_from / gen_date_to query params filtering by date(created_at) — separate from start_date event date filters Frontend (MarketEvents.tsx): - MarketEvent interface: add created_at field - EventRow: show generation date as ⚡MM-DD next to event date - Extended filters: new '⚡ Date de génération' section with from/to inputs filtered independently from the event date range - Clear-all button includes genFrom/genTo reset Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,45 @@ _SORT_COLS = {
|
|||||||
"instrument_score": "COALESCE(ii.adjusted_score, ii.impact_score)",
|
"instrument_score": "COALESCE(ii.adjusted_score, ii.impact_score)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@router.get("/db-status")
|
||||||
|
def db_status() -> Dict[str, Any]:
|
||||||
|
"""Quick health-check of all tables needed for market event generation."""
|
||||||
|
from services.database import get_conn
|
||||||
|
conn = get_conn()
|
||||||
|
status: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
tables = {
|
||||||
|
"market_events": "SELECT COUNT(*), MAX(created_at) FROM market_events",
|
||||||
|
"economic_events": "SELECT COUNT(*), MAX(fetched_at) FROM economic_events",
|
||||||
|
"macro_gauge_snapshots":"SELECT COUNT(*), MAX(snapshot_date) FROM macro_gauge_snapshots",
|
||||||
|
"macro_regime_history": "SELECT COUNT(*), MAX(timestamp) FROM macro_regime_history",
|
||||||
|
"institutional_reports":"SELECT COUNT(*), MAX(fetch_date) FROM institutional_reports",
|
||||||
|
"ai_desks": "SELECT COUNT(*), NULL FROM ai_desks",
|
||||||
|
}
|
||||||
|
for table, sql in tables.items():
|
||||||
|
try:
|
||||||
|
row = conn.execute(sql).fetchone()
|
||||||
|
status[table] = {"count": row[0], "latest": row[1]}
|
||||||
|
except Exception as e:
|
||||||
|
status[table] = {"count": 0, "latest": None, "error": str(e)}
|
||||||
|
|
||||||
|
# Check market_events columns
|
||||||
|
existing_cols = {row[1] for row in conn.execute("PRAGMA table_info(market_events)").fetchall()}
|
||||||
|
required_cols = ["sub_type", "origin", "source_refs", "actual_value", "expected_value", "surprise_pct"]
|
||||||
|
status["market_events"]["columns_ok"] = all(c in existing_cols for c in required_cols)
|
||||||
|
status["market_events"]["missing_columns"] = [c for c in required_cols if c not in existing_cols]
|
||||||
|
|
||||||
|
# Summary of ai_desks types
|
||||||
|
try:
|
||||||
|
desks = conn.execute("SELECT type, active FROM ai_desks").fetchall()
|
||||||
|
status["ai_desks"]["desks"] = [{"type": r[0], "active": bool(r[1])} for r in desks]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def list_events(
|
def list_events(
|
||||||
# text / category / level
|
# text / category / level
|
||||||
@@ -91,9 +130,12 @@ def list_events(
|
|||||||
category: Optional[str] = Query(None),
|
category: Optional[str] = Query(None),
|
||||||
level: Optional[str] = Query(None),
|
level: Optional[str] = Query(None),
|
||||||
origin: Optional[str] = Query(None),
|
origin: Optional[str] = Query(None),
|
||||||
# date range
|
# event date range (start_date)
|
||||||
date_from: Optional[str] = Query(None),
|
date_from: Optional[str] = Query(None),
|
||||||
date_to: Optional[str] = Query(None),
|
date_to: Optional[str] = Query(None),
|
||||||
|
# generation date range (created_at)
|
||||||
|
gen_date_from: Optional[str] = Query(None, description="Filter by generation date (created_at) from"),
|
||||||
|
gen_date_to: Optional[str] = Query(None, description="Filter by generation date (created_at) to"),
|
||||||
# global impact score
|
# global impact score
|
||||||
min_score: float = Query(0.0, ge=0.0, le=1.0),
|
min_score: float = Query(0.0, ge=0.0, le=1.0),
|
||||||
# instrument impact filter
|
# instrument impact filter
|
||||||
@@ -143,6 +185,12 @@ def list_events(
|
|||||||
if date_to:
|
if date_to:
|
||||||
where_parts.append("me.start_date<=?")
|
where_parts.append("me.start_date<=?")
|
||||||
params.append(date_to)
|
params.append(date_to)
|
||||||
|
if gen_date_from:
|
||||||
|
where_parts.append("date(me.created_at)>=?")
|
||||||
|
params.append(gen_date_from)
|
||||||
|
if gen_date_to:
|
||||||
|
where_parts.append("date(me.created_at)<=?")
|
||||||
|
params.append(gen_date_to)
|
||||||
if min_score > 0:
|
if min_score > 0:
|
||||||
where_parts.append("me.impact_score>=?")
|
where_parts.append("me.impact_score>=?")
|
||||||
params.append(min_score)
|
params.append(min_score)
|
||||||
|
|||||||
@@ -774,6 +774,8 @@ def init_db():
|
|||||||
("surprise_pct", "REAL DEFAULT NULL"),
|
("surprise_pct", "REAL DEFAULT NULL"),
|
||||||
("unit", "TEXT DEFAULT NULL"),
|
("unit", "TEXT DEFAULT NULL"),
|
||||||
("sub_type", "TEXT DEFAULT NULL"),
|
("sub_type", "TEXT DEFAULT NULL"),
|
||||||
|
("origin", "TEXT DEFAULT NULL"),
|
||||||
|
("source_refs", "TEXT DEFAULT '[]'"),
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
c.execute(f"ALTER TABLE market_events ADD COLUMN {_col} {_def}")
|
c.execute(f"ALTER TABLE market_events ADD COLUMN {_col} {_def}")
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ interface MarketEvent {
|
|||||||
absorption_pct: number | null
|
absorption_pct: number | null
|
||||||
source_refs: SourceRef[]
|
source_refs: SourceRef[]
|
||||||
origin?: string
|
origin?: string
|
||||||
|
created_at?: string
|
||||||
impacts?: InstrumentImpact[]
|
impacts?: InstrumentImpact[]
|
||||||
evaluated?: boolean
|
evaluated?: boolean
|
||||||
}
|
}
|
||||||
@@ -780,6 +781,11 @@ function EventRow({
|
|||||||
{(ORIGIN_META[ev.origin] ?? ORIGIN_META['unknown']).icon}
|
{(ORIGIN_META[ev.origin] ?? ORIGIN_META['unknown']).icon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{ev.created_at && (
|
||||||
|
<span className="text-[10px] text-slate-700 shrink-0" title={`Généré le ${ev.created_at}`}>
|
||||||
|
⚡{ev.created_at.slice(5, 10)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 text-right">
|
<div className="shrink-0 text-right">
|
||||||
@@ -1130,6 +1136,9 @@ export default function MarketEvents() {
|
|||||||
const [dateFrom, setDateFrom] = useState('')
|
const [dateFrom, setDateFrom] = useState('')
|
||||||
const [dateTo, setDateTo] = useState('')
|
const [dateTo, setDateTo] = useState('')
|
||||||
const [datePreset, setDatePreset] = useState<string>('all') // 7d|30d|90d|6m|1y|all|custom
|
const [datePreset, setDatePreset] = useState<string>('all') // 7d|30d|90d|6m|1y|all|custom
|
||||||
|
// Generation date filter (created_at)
|
||||||
|
const [genFrom, setGenFrom] = useState('')
|
||||||
|
const [genTo, setGenTo] = useState('')
|
||||||
// Instrument impact filter
|
// Instrument impact filter
|
||||||
const [instTicker, setInstTicker] = useState('')
|
const [instTicker, setInstTicker] = useState('')
|
||||||
const [instMinScore, setInstMin] = useState(0.3)
|
const [instMinScore, setInstMin] = useState(0.3)
|
||||||
@@ -1177,6 +1186,8 @@ export default function MarketEvents() {
|
|||||||
if (minScore) params.set('min_score', String(minScore))
|
if (minScore) params.set('min_score', String(minScore))
|
||||||
if (dateFrom) params.set('date_from', dateFrom)
|
if (dateFrom) params.set('date_from', dateFrom)
|
||||||
if (dateTo) params.set('date_to', dateTo)
|
if (dateTo) params.set('date_to', dateTo)
|
||||||
|
if (genFrom) params.set('gen_date_from', genFrom)
|
||||||
|
if (genTo) params.set('gen_date_to', genTo)
|
||||||
if (evaluated) params.set('evaluated', evaluated)
|
if (evaluated) params.set('evaluated', evaluated)
|
||||||
if (instTicker) {
|
if (instTicker) {
|
||||||
params.set('instrument', instTicker)
|
params.set('instrument', instTicker)
|
||||||
@@ -1197,7 +1208,7 @@ export default function MarketEvents() {
|
|||||||
if (e.name === 'AbortError') return // superseded by a newer load
|
if (e.name === 'AbortError') return // superseded by a newer load
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, [search, category, level, minScore, dateFrom, dateTo, evaluated,
|
}, [search, category, level, minScore, dateFrom, dateTo, genFrom, genTo, evaluated,
|
||||||
instTicker, instMinScore, instDir, sortBy, sortDir])
|
instTicker, instMinScore, instDir, sortBy, sortDir])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1418,6 +1429,34 @@ export default function MarketEvents() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Generation date filter ── */}
|
||||||
|
<div className="border-t border-slate-800/60 pt-2">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-semibold mb-2 text-slate-500">
|
||||||
|
⚡ Date de génération
|
||||||
|
{(genFrom || genTo) && (
|
||||||
|
<button onClick={() => { setGenFrom(''); setGenTo('') }}
|
||||||
|
className="ml-auto text-slate-600 hover:text-red-400 font-normal">× effacer</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-600 mb-0.5">Généré depuis</div>
|
||||||
|
<input type="date" value={genFrom} onChange={e => setGenFrom(e.target.value)}
|
||||||
|
className="w-full bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-300 text-xs" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-600 mb-0.5">Généré jusqu'à</div>
|
||||||
|
<input type="date" value={genTo} onChange={e => setGenTo(e.target.value)}
|
||||||
|
className="w-full bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-300 text-xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(genFrom || genTo) && (
|
||||||
|
<div className="text-xs text-blue-300 bg-blue-900/20 rounded px-2 py-1 border border-blue-800/30 mt-1.5">
|
||||||
|
⚡ Filtre actif sur date de génération (created_at)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1439,10 +1478,11 @@ export default function MarketEvents() {
|
|||||||
<div className="px-3 py-1 text-xs text-slate-600 border-b border-slate-800/30 flex items-center gap-2">
|
<div className="px-3 py-1 text-xs text-slate-600 border-b border-slate-800/30 flex items-center gap-2">
|
||||||
<span>{loading ? '...' : total} événement{total !== 1 ? 's' : ''}</span>
|
<span>{loading ? '...' : total} événement{total !== 1 ? 's' : ''}</span>
|
||||||
{instTicker && <span className="text-amber-400">· impact {instTicker}</span>}
|
{instTicker && <span className="text-amber-400">· impact {instTicker}</span>}
|
||||||
{(category || level || search || minScore > 0 || evaluated || dateFrom || dateTo || instTicker) && (
|
{(category || level || search || minScore > 0 || evaluated || dateFrom || dateTo || genFrom || genTo || instTicker) && (
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
setSearch(''); setCategory(''); setLevel(''); setMinScore(0)
|
setSearch(''); setCategory(''); setLevel(''); setMinScore(0)
|
||||||
setEvaluated(''); setDateFrom(''); setDateTo(''); setDatePreset('all')
|
setEvaluated(''); setDateFrom(''); setDateTo(''); setDatePreset('all')
|
||||||
|
setGenFrom(''); setGenTo('')
|
||||||
setInstTicker(''); setInstDir(''); setInstMin(0.3); setSortBy('date')
|
setInstTicker(''); setInstDir(''); setInstMin(0.3); setSortBy('date')
|
||||||
}} className="ml-auto text-xs text-slate-600 hover:text-red-400">× tout effacer</button>
|
}} className="ml-auto text-xs text-slate-600 hover:text-red-400">× tout effacer</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user