feat: pressure
This commit is contained in:
@@ -4,7 +4,7 @@ Exposes per-instrument snapshot (price, indicators, regime, trend, events) and A
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, date as date_type
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any, Optional
|
||||
@@ -227,3 +227,166 @@ def get_theoretical_curve(
|
||||
result.append({"date": d, **entry})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ── Libellés lisibles par catégorie ───────────────────────────────────────────
|
||||
_CAT_LABELS: Dict[str, str] = {
|
||||
"central_bank": "Banque Centrale",
|
||||
"monetary_shock": "Surprise Macro",
|
||||
"geopolitical": "Géopolitique",
|
||||
"commodity": "Commodités",
|
||||
"growth_shock": "Croissance",
|
||||
"trade_policy": "Commerce / Tarifs",
|
||||
"credit_stress": "Stress Crédit",
|
||||
"sentiment": "Sentiment & Position.",
|
||||
"technical": "Technique",
|
||||
"positioning": "Flux Institutionnels",
|
||||
"unclassified": "Non Classifié",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{instrument_id}/factor-state")
|
||||
def get_factor_state(
|
||||
instrument_id: str,
|
||||
at_date: Optional[str] = Query(None, description="YYYY-MM-DD (défaut: aujourd'hui)"),
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Pression nette actuelle sur l'instrument : somme de toutes les contributions
|
||||
d'events actifs pondérées par leur courbe de dissipation.
|
||||
|
||||
Retourne une décomposition par catégorie causale (Banque Centrale, Surprise Macro…)
|
||||
avec détail par event, ainsi que le NET en pips et la direction.
|
||||
"""
|
||||
from services.database import get_conn
|
||||
|
||||
try:
|
||||
ref_date = date_type.fromisoformat(at_date) if at_date else datetime.utcnow().date()
|
||||
except ValueError:
|
||||
ref_date = datetime.utcnow().date()
|
||||
|
||||
# Cherche les analyses pour cet instrument dans les 180 jours précédents
|
||||
extended_from = ref_date - timedelta(days=180)
|
||||
inst_upper = instrument_id.upper()
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
rows = conn.execute("""
|
||||
SELECT a.prediction_json,
|
||||
e.start_date AS event_date,
|
||||
e.name AS event_name,
|
||||
e.sub_type AS event_sub_type,
|
||||
e.end_date AS event_end_date,
|
||||
t.name AS template_name,
|
||||
t.category AS category,
|
||||
t.calibration_json
|
||||
FROM causal_event_analyses a
|
||||
JOIN market_events e ON e.id = a.market_event_id
|
||||
JOIN causal_graph_templates t ON t.id = a.template_id
|
||||
WHERE a.instrument = ?
|
||||
AND e.start_date >= ?
|
||||
AND e.start_date <= ?
|
||||
ORDER BY e.start_date DESC
|
||||
""", (inst_upper, str(extended_from), str(ref_date))).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
inst_lower = inst_upper.lower()
|
||||
by_category: Dict[str, Dict] = {}
|
||||
seen_events: set = set() # évite les doublons (même event × multi-analyse)
|
||||
|
||||
for row in rows:
|
||||
r = dict(row)
|
||||
event_key = (r["event_name"], r["event_date"])
|
||||
if event_key in seen_events:
|
||||
continue
|
||||
seen_events.add(event_key)
|
||||
|
||||
try:
|
||||
predictions = json.loads(r["prediction_json"] or "{}")
|
||||
calib = json.loads(r["calibration_json"] or "{}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Pips prédits pour cet instrument (cherche node_id == inst_lower ou contenant)
|
||||
pips_full: Optional[float] = None
|
||||
if inst_lower in predictions:
|
||||
pips_full = float(predictions[inst_lower])
|
||||
else:
|
||||
for k, v in predictions.items():
|
||||
if inst_lower in k.lower():
|
||||
try:
|
||||
pips_full = float(v)
|
||||
break
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
if pips_full is None or pips_full == 0:
|
||||
continue
|
||||
|
||||
absorption_days = max(1, int(calib.get("absorption_days", 7)))
|
||||
decay_type = str(calib.get("decay_type", "exp"))
|
||||
|
||||
try:
|
||||
ev_date = date_type.fromisoformat(r["event_date"][:10])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Pour les guidance events : end_date = meeting date → absorption dynamique
|
||||
ev_end = r.get("event_end_date")
|
||||
if ev_end and r.get("event_sub_type", "").startswith("rate_guidance"):
|
||||
try:
|
||||
meeting = date_type.fromisoformat(ev_end[:10])
|
||||
absorption_days = max(1, (meeting - ev_date).days)
|
||||
decay_type = "linear" # anticipation linéaire jusqu'à la réunion
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
days_elapsed = (ref_date - ev_date).days
|
||||
df = _decay(days_elapsed, absorption_days, decay_type)
|
||||
if df < 0.01:
|
||||
continue
|
||||
|
||||
current_pips = round(pips_full * df, 1)
|
||||
cat = r["category"]
|
||||
|
||||
if cat not in by_category:
|
||||
by_category[cat] = {
|
||||
"label": _CAT_LABELS.get(cat, cat),
|
||||
"pips": 0.0,
|
||||
"contributions": [],
|
||||
}
|
||||
|
||||
by_category[cat]["pips"] += current_pips
|
||||
by_category[cat]["contributions"].append({
|
||||
"event_name": r["event_name"],
|
||||
"event_date": r["event_date"][:10],
|
||||
"template_name": r["template_name"],
|
||||
"pips_full": round(pips_full, 1),
|
||||
"days_elapsed": days_elapsed,
|
||||
"absorption_days": absorption_days,
|
||||
"decay_pct": round(df * 100),
|
||||
"pips_current": current_pips,
|
||||
})
|
||||
|
||||
# Arrondi + tri par |pips| décroissant
|
||||
for v in by_category.values():
|
||||
v["pips"] = round(v["pips"], 1)
|
||||
v["contributions"].sort(key=lambda c: abs(c["pips_current"]), reverse=True)
|
||||
|
||||
categories = sorted(by_category.values(), key=lambda x: abs(x["pips"]), reverse=True)
|
||||
net_pips = round(sum(v["pips"] for v in by_category.values()), 1)
|
||||
|
||||
direction = "neutral"
|
||||
if net_pips > 5:
|
||||
direction = "bullish"
|
||||
elif net_pips < -5:
|
||||
direction = "bearish"
|
||||
|
||||
return {
|
||||
"instrument": inst_upper,
|
||||
"at_date": str(ref_date),
|
||||
"net_pips": net_pips,
|
||||
"direction": direction,
|
||||
"categories": categories,
|
||||
"n_events": len(seen_events),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user