feat: instrument model
This commit is contained in:
@@ -229,93 +229,108 @@ def timeline_whatif(
|
||||
conn.close()
|
||||
|
||||
|
||||
_INSTRUMENT_CURRENCIES: Dict[str, List[str]] = {
|
||||
"EURUSD": ["EUR", "USD"],
|
||||
"USDJPY": ["USD", "JPY"],
|
||||
"XAUUSD": ["USD"],
|
||||
"SP500": ["USD"],
|
||||
"TLT": ["USD"],
|
||||
"GBPUSD": ["GBP", "USD"],
|
||||
"EEM": ["USD", "CNY"],
|
||||
"QQQ": ["USD"],
|
||||
}
|
||||
|
||||
|
||||
def _ff_event_to_category(event_name: str) -> str:
|
||||
n = event_name.lower()
|
||||
if any(k in n for k in ["fomc", "fed ", "ecb", "boe", "boj", "rba", "interest rate", "rate decision", "monetary policy", "central bank"]):
|
||||
return "central_bank"
|
||||
if any(k in n for k in ["cpi", "pce", "ppi", "inflation", "core price"]):
|
||||
return "monetary_shock"
|
||||
if any(k in n for k in ["nfp", "non-farm", "payroll", "employment change", "unemployment"]):
|
||||
return "monetary_shock"
|
||||
if any(k in n for k in ["gdp", "growth", "retail sales", "manufacturing", "pmi", "ism"]):
|
||||
return "growth_shock"
|
||||
if any(k in n for k in ["oil", "opec", "crude", "energy", "natural gas"]):
|
||||
return "commodity"
|
||||
if any(k in n for k in ["tarif", "trade war", "sanction", "geopolit"]):
|
||||
return "geopolitical"
|
||||
return "unclassified"
|
||||
|
||||
|
||||
@router.get("/{instrument}/calendar-events")
|
||||
def get_calendar_events(
|
||||
instrument: str,
|
||||
days_back: int = Query(365, description="Jours en arrière depuis at_date"),
|
||||
days_forward: int = Query(90, description="Jours en avant depuis at_date"),
|
||||
at_date: Optional[str]= Query(None),
|
||||
instrument: str,
|
||||
days_back: int = Query(90, description="Jours en arriere depuis at_date"),
|
||||
days_forward: int = Query(180, description="Jours en avant depuis at_date"),
|
||||
impacts: Optional[str] = Query("high,medium", description="Filtre impact FF (high,medium,low)"),
|
||||
at_date: Optional[str] = Query(None),
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Events calendrier analysés pour cet instrument — alimente le panneau What-if."""
|
||||
"""
|
||||
Events economiques du calendrier Forex Factory pour cet instrument.
|
||||
Source : ff_calendar (Forex Factory). Decorrele des market_events / CausalLab.
|
||||
Auto-sync live si ff_calendar vide pour la periode demandee.
|
||||
"""
|
||||
from services.database import get_conn
|
||||
from services.instrument_models import _lifecycle, init_instrument_model_tables
|
||||
from datetime import datetime, date as date_type, timedelta
|
||||
import json as _json
|
||||
conn = get_conn()
|
||||
try:
|
||||
inst_upper = instrument.upper()
|
||||
inst_lower = inst_upper.lower()
|
||||
init_instrument_model_tables(conn)
|
||||
try:
|
||||
ref = date_type.fromisoformat(at_date) if at_date else datetime.utcnow().date()
|
||||
except ValueError:
|
||||
ref = datetime.utcnow().date()
|
||||
date_from = ref - timedelta(days=days_back)
|
||||
date_to = ref + timedelta(days=days_forward)
|
||||
|
||||
rows = conn.execute("""
|
||||
SELECT a.id as analysis_id,
|
||||
a.prediction_json,
|
||||
e.id as event_id, e.name as title, e.start_date, e.end_date, e.sub_type,
|
||||
t.category,
|
||||
COALESCE(o.calibration_json, t.calibration_json) as 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
|
||||
LEFT JOIN event_calibration_overrides o ON o.analysis_id = a.id
|
||||
WHERE a.instrument = ?
|
||||
AND e.start_date >= ?
|
||||
AND e.start_date <= ?
|
||||
ORDER BY e.start_date DESC
|
||||
""", (inst_upper, str(date_from), str(date_to))).fetchall()
|
||||
date_from = str(ref - timedelta(days=days_back))
|
||||
date_to = str(ref + timedelta(days=days_forward))
|
||||
|
||||
currencies = _INSTRUMENT_CURRENCIES.get(inst_upper, ["USD"])
|
||||
impact_filter = [i.strip() for i in impacts.split(",")] if impacts else ["high", "medium"]
|
||||
|
||||
# Auto-sync live si ff_calendar vide pour cette periode
|
||||
ph = ",".join("?" * len(currencies))
|
||||
n_existing = conn.execute(
|
||||
f"SELECT COUNT(*) FROM ff_calendar WHERE event_date>=? AND event_date<=? AND currency IN ({ph})",
|
||||
[date_from, date_to, *currencies]
|
||||
).fetchone()[0]
|
||||
|
||||
if n_existing == 0:
|
||||
try:
|
||||
from services.ff_calendar import sync_live
|
||||
sync_live()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pi = ",".join("?" * len(impact_filter))
|
||||
rows = conn.execute(
|
||||
f"""SELECT event_date, event_time, currency, impact, event_name,
|
||||
actual_value, forecast_value, previous_value
|
||||
FROM ff_calendar
|
||||
WHERE event_date >= ? AND event_date <= ?
|
||||
AND currency IN ({ph})
|
||||
AND impact IN ({pi})
|
||||
ORDER BY event_date ASC, event_time ASC""",
|
||||
[date_from, date_to, *currencies, *impact_filter]
|
||||
).fetchall()
|
||||
|
||||
result = []
|
||||
for row in rows:
|
||||
r = dict(row)
|
||||
try:
|
||||
preds = _json.loads(r["prediction_json"] or "{}")
|
||||
calib = _json.loads(r["calibration_json"] or "{}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
pips: float = 0.0
|
||||
if inst_lower in preds:
|
||||
pips = float(preds[inst_lower])
|
||||
else:
|
||||
for k, v in preds.items():
|
||||
if inst_lower in k.lower():
|
||||
try: pips = float(v); break
|
||||
except (TypeError, ValueError): pass
|
||||
|
||||
try:
|
||||
ev_date = date_type.fromisoformat(r["start_date"][:10])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
days = (ref - ev_date).days
|
||||
absorption = max(1, int(calib.get("absorption_days", 7)))
|
||||
dtype = str(calib.get("decay_type", "exp"))
|
||||
rise = max(0, int(calib.get("rise_days", 0)))
|
||||
plateau = max(0, int(calib.get("plateau_days", 0)))
|
||||
lf = _lifecycle(days, rise, plateau, absorption, dtype) if days >= 0 else 0.0
|
||||
|
||||
r = dict(row)
|
||||
ev_date = r["event_date"]
|
||||
is_future = ev_date > str(ref)
|
||||
category = _ff_event_to_category(r["event_name"])
|
||||
result.append({
|
||||
"analysis_id": r["analysis_id"],
|
||||
"event_id": r.get("event_id"),
|
||||
"title": r.get("title") or r.get("sub_type", ""),
|
||||
"start_date": r["start_date"][:10],
|
||||
"category": r["category"],
|
||||
"is_future": ev_date > ref,
|
||||
"days_offset": -days, # positive = future, negative = past
|
||||
"pip_prediction": round(pips, 1),
|
||||
"lifecycle_factor": round(lf, 3),
|
||||
"remaining_pips": round(pips * lf, 1),
|
||||
"calibration": {
|
||||
"absorption_days": absorption,
|
||||
"rise_days": rise,
|
||||
"plateau_days": plateau,
|
||||
"decay_type": dtype,
|
||||
},
|
||||
"date": ev_date,
|
||||
"event_time": r.get("event_time", ""),
|
||||
"currency": r["currency"],
|
||||
"impact": r["impact"],
|
||||
"event_name": r["event_name"],
|
||||
"label": f"[{r['currency']}] {r['event_name']}",
|
||||
"actual_value": r.get("actual_value"),
|
||||
"forecast_value": r.get("forecast_value"),
|
||||
"previous_value": r.get("previous_value"),
|
||||
"category": category,
|
||||
"is_future": is_future,
|
||||
})
|
||||
return result
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user