feat: origin tracing on all market_events

- DB: colonne origin (migration + UPDATE heuristique sur données legacy)
- save/update_market_event: persist origin
- Tous les points de création taguent leur origine:
    bootstrap_macro/eco/ma/legacy | detector_news/eco/technical/report | manual
- UI MarketEvents: badge d'origine avec icône + description dans le panneau détail,
  icône tooltip dans la liste gauche, message explicite si pas de source_refs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-25 20:58:05 +02:00
parent d5da4737ef
commit bd28b6a73a
8 changed files with 78 additions and 16 deletions

View File

@@ -139,6 +139,7 @@ def create_event(body: EventCreate) -> Dict[str, Any]:
from services.database import save_market_event
data = body.dict()
data["source_refs"] = [r.dict() for r in body.source_refs]
data.setdefault("origin", "manual")
new_id = save_market_event(data)
return {"id": new_id, "status": "created"}

View File

@@ -43,7 +43,9 @@ def list_events() -> List[Dict[str, Any]]:
@router.post("/events")
def create_event(body: EventCreate) -> Dict[str, Any]:
from services.database import save_market_event
new_id = save_market_event(body.dict())
data = body.dict()
data.setdefault("origin", "manual")
new_id = save_market_event(data)
return {"id": new_id, "status": "created"}

View File

@@ -945,12 +945,22 @@ def init_db():
# Idempotent column additions
for _col_sql in [
"ALTER TABLE market_events ADD COLUMN source_refs TEXT DEFAULT '[]'",
"ALTER TABLE market_events ADD COLUMN origin TEXT DEFAULT 'unknown'",
]:
try:
c.execute(_col_sql)
except Exception:
pass
# Best-effort origin classification for legacy rows (runs once, idempotent)
try:
c.execute("UPDATE market_events SET origin='bootstrap_eco' WHERE origin='unknown' AND category='event_calendar'")
c.execute("UPDATE market_events SET origin='bootstrap_ma' WHERE origin='unknown' AND category='technical'")
c.execute("UPDATE market_events SET origin='bootstrap_macro' WHERE origin='unknown' AND category IN ('geopolitical','fundamental','report','sentiment')")
c.execute("UPDATE market_events SET origin='bootstrap_legacy' WHERE origin='unknown'")
except Exception:
pass
conn.commit()
conn.close()
@@ -4561,15 +4571,16 @@ def save_market_event(ev: Dict[str, Any]) -> int:
(name, start_date, end_date, level, category, description, market_impact,
affected_assets, impact_score, parent_event_id,
expected_value, actual_value, surprise_pct, unit, sub_type, absorption_pct,
source_refs)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
source_refs, origin)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(ev["name"], ev["start_date"], ev.get("end_date"),
ev["level"], ev.get("category", "macro"), ev.get("description", ""),
ev.get("market_impact", ""), json.dumps(ev.get("affected_assets", [])),
ev.get("impact_score", 0.5), ev.get("parent_event_id"),
ev.get("expected_value"), ev.get("actual_value"),
ev.get("surprise_pct"), ev.get("unit"), ev.get("sub_type"),
ev.get("absorption_pct"), src_refs))
ev.get("absorption_pct"), src_refs,
ev.get("origin", "unknown")))
conn.commit()
return cur.lastrowid
finally:
@@ -4586,14 +4597,14 @@ def update_market_event(event_id: int, ev: Dict[str, Any]) -> bool:
conn.execute("""UPDATE market_events SET
name=?, start_date=?, end_date=?, level=?, category=?, description=?,
market_impact=?, affected_assets=?, impact_score=?,
absorption_pct=?, relevant_indicators=?, source_refs=?
absorption_pct=?, relevant_indicators=?, source_refs=?, origin=?
WHERE id=?""",
(ev["name"], ev["start_date"], ev.get("end_date"),
ev["level"], ev.get("category", "macro"), ev.get("description", ""),
ev.get("market_impact", ""), json.dumps(ev.get("affected_assets", [])),
ev.get("impact_score", 0.5),
ev.get("absorption_pct"), json.dumps(ev.get("relevant_indicators", [])),
src_refs, event_id))
src_refs, ev.get("origin", "unknown"), event_id))
conn.commit()
return True
finally:

View File

@@ -574,6 +574,7 @@ def bootstrap_eco_events(force: bool = False) -> Dict[str, Any]:
"surprise_pct": ev.get("surprise_pct"),
"unit": ev.get("unit"),
"absorption_pct": ev.get("absorption_pct", 100),
"origin": "bootstrap_eco",
})
inserted += 1
except Exception as e:

View File

@@ -491,6 +491,7 @@ def _to_event_dict(ev: Dict[str, Any]) -> Dict[str, Any]:
"impact_score": ev.get("impact_score", 0.4),
"absorption_pct": None,
"relevant_indicators": [],
"origin": "bootstrap_ma",
}

View File

@@ -288,6 +288,7 @@ def bootstrap_macro_events(force: bool = False) -> Dict[str, Any]:
"market_impact": ev["description"][:80],
"affected_assets": assets_list,
"impact_score": ev.get("impact_score", 0.75),
"origin": "bootstrap_macro",
})
inserted += 1
except Exception as e:

View File

@@ -203,6 +203,7 @@ FORMAT JSON STRICT:
"affected_assets": parsed.get("affected_assets", []),
"impact_score": float(parsed.get("impact_score", 0.6)),
"source_refs": [source_ref],
"origin": "detector_news",
}
result = _save_and_evaluate(ev, existing)
if result:
@@ -276,6 +277,7 @@ def _check_eco(z_threshold: float = 1.5, days: int = 7) -> List[Dict[str, Any]]:
"expected_value": str(rel.get("forecast_value", "")),
"surprise_pct": float(s_pct),
"source_refs": [source_ref],
"origin": "detector_eco",
}
result = _save_and_evaluate(ev, existing)
if result:
@@ -376,6 +378,7 @@ def _check_technical(instruments: List[str] = None, lookback_days: int = 7) -> L
"affected_assets": [ticker],
"impact_score": 0.65 if slow_lbl == "MA200" else 0.45,
"source_refs": [source_ref],
"origin": "detector_technical",
}
result = _save_and_evaluate(ev, existing)
if result:
@@ -458,6 +461,7 @@ def _check_reports(days: int = 7, min_importance: int = 3) -> List[Dict[str, Any
"affected_assets": list(set(assets)),
"impact_score": min(0.9, 0.3 + rpt.get("importance", 2) * 0.12),
"source_refs": [source_ref],
"origin": "detector_report",
}
result = _save_and_evaluate(ev, existing)
if result: