feat: instrument model
This commit is contained in:
@@ -148,6 +148,43 @@ def apply_gauge_suggestions(
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/{instrument}/macro-sync")
|
||||
def get_macro_sync(instrument: str) -> Dict[str, Any]:
|
||||
"""Retourne les valeurs actuelles + forecasts FF Calendar pour tous les nœuds macro-liés."""
|
||||
from services.database import get_conn
|
||||
from services.instrument_models import get_macro_sync_items
|
||||
conn = get_conn()
|
||||
try:
|
||||
items = get_macro_sync_items(conn, instrument)
|
||||
return {"instrument": instrument.upper(), "items": items, "count": len(items)}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class MacroSyncApplyBody(BaseModel):
|
||||
items: list[dict] # [{node_id, value, note}]
|
||||
|
||||
@router.post("/{instrument}/macro-sync/apply")
|
||||
def apply_macro_sync(instrument: str, body: MacroSyncApplyBody) -> Dict[str, Any]:
|
||||
"""Applique les valeurs macro (actuelles ou forecasts) comme node overrides."""
|
||||
from services.database import get_conn
|
||||
from services.instrument_models import set_node_override
|
||||
conn = get_conn()
|
||||
try:
|
||||
inst = instrument.upper()
|
||||
saved = []
|
||||
for item in body.items:
|
||||
nid = item.get("node_id", "")
|
||||
value = item.get("value")
|
||||
note = item.get("note", "[Sync Marché FF Calendar]")
|
||||
if nid and value is not None:
|
||||
set_node_override(conn, inst, nid, float(value), note)
|
||||
saved.append(nid)
|
||||
return {"ok": True, "instrument": inst, "saved": saved, "count": len(saved)}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.delete("/{instrument}/overrides")
|
||||
def clear_all_overrides(instrument: str) -> Dict[str, Any]:
|
||||
"""Supprime TOUTES les overrides d'un instrument (reset à zéro)."""
|
||||
|
||||
@@ -315,6 +315,132 @@ def build_node_combined_timeline(
|
||||
return result
|
||||
|
||||
|
||||
# ── Macro sync — lecture directe ff_calendar → overrides ──────────────────────
|
||||
|
||||
def get_macro_sync_items(conn, instrument: str) -> list[dict]:
|
||||
"""
|
||||
Pour chaque nœud avec macro_key dans le graphe, retourne :
|
||||
- actual_value : dernière valeur publiée (passé)
|
||||
- actual_date : date de cette publication
|
||||
- forecast_value: forecast du prochain événement (futur)
|
||||
- forecast_date / next_event_name / days_until
|
||||
- current_override : valeur déjà settée en DB (ou None)
|
||||
Utilisé par le bouton "Sync Marché" pour pré-remplir le graphe.
|
||||
"""
|
||||
inst_upper = instrument.upper()
|
||||
row = conn.execute(
|
||||
"SELECT graph_json FROM instrument_models WHERE instrument=?", (inst_upper,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
return []
|
||||
|
||||
graph_def = json.loads(row["graph_json"])
|
||||
today = date_type.today()
|
||||
today_str = str(today)
|
||||
q_from = str(today - timedelta(days=730))
|
||||
q_to = str(today + timedelta(days=180))
|
||||
|
||||
overrides = {r["node_id"]: float(r["value"]) for r in conn.execute(
|
||||
"SELECT node_id, value FROM instrument_node_overrides WHERE instrument=?", (inst_upper,)
|
||||
).fetchall()}
|
||||
|
||||
items = []
|
||||
for node in graph_def.get("nodes", []):
|
||||
mk = node.get("macro_key")
|
||||
if not mk or node.get("node_type") != "input_manual":
|
||||
continue
|
||||
mk_info = FF_MACRO_KEYS.get(mk)
|
||||
if not mk_info:
|
||||
continue
|
||||
|
||||
currency = mk_info["currency"]
|
||||
patterns = mk_info["names"]
|
||||
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""SELECT event_date, event_name, actual_value, forecast_value, previous_value
|
||||
FROM ff_calendar
|
||||
WHERE currency=? AND event_date>=? AND event_date<=?
|
||||
ORDER BY event_date ASC""",
|
||||
(currency, q_from, q_to)
|
||||
).fetchall()
|
||||
except Exception:
|
||||
rows = []
|
||||
|
||||
matched = [
|
||||
dict(r) for r in rows
|
||||
if any(p in str(r["event_name"]).lower() for p in patterns)
|
||||
]
|
||||
|
||||
# Deduplicate by date
|
||||
seen: set[str] = set()
|
||||
deduped = []
|
||||
for ev in matched:
|
||||
if ev["event_date"] not in seen:
|
||||
seen.add(ev["event_date"])
|
||||
deduped.append(ev)
|
||||
|
||||
# Latest actual (most recent past event with actual_value)
|
||||
past = [e for e in deduped if e["event_date"] <= today_str]
|
||||
actual_val: Optional[float] = None
|
||||
actual_date: Optional[str] = None
|
||||
for ev in reversed(past):
|
||||
v = _parse_ff_num(ev.get("actual_value"))
|
||||
if v is not None:
|
||||
actual_val = v
|
||||
actual_date = ev["event_date"]
|
||||
break
|
||||
|
||||
# Next forecast (first future event)
|
||||
future = [e for e in deduped if e["event_date"] > today_str]
|
||||
forecast_val: Optional[float] = None
|
||||
forecast_date: Optional[str] = None
|
||||
next_event_name: Optional[str] = None
|
||||
days_until: Optional[int] = None
|
||||
if future:
|
||||
nxt = future[0]
|
||||
forecast_val = (
|
||||
_parse_ff_num(nxt.get("forecast_value"))
|
||||
or _parse_ff_num(nxt.get("previous_value"))
|
||||
)
|
||||
forecast_date = nxt["event_date"]
|
||||
next_event_name = nxt["event_name"]
|
||||
try:
|
||||
days_until = (date_type.fromisoformat(forecast_date) - today).days
|
||||
except Exception:
|
||||
days_until = None
|
||||
|
||||
# Unit-aware post-conversion
|
||||
# The FF Calendar stores NFP in absolute (e.g. 228000), node expects K (228)
|
||||
# PMI stored as absolute value (54.7), node expects deviation from 50 (4.7)
|
||||
unit = node.get("unit", "")
|
||||
def _convert(v: Optional[float]) -> Optional[float]:
|
||||
if v is None:
|
||||
return None
|
||||
if unit == "K" and abs(v) >= 1_000:
|
||||
return round(v / 1_000, 1)
|
||||
if unit == "pts" and v > 10: # PMI > 10 → is absolute, subtract 50
|
||||
return round(v - 50.0, 1)
|
||||
return round(v, 4)
|
||||
|
||||
items.append({
|
||||
"node_id": node["id"],
|
||||
"node_label": node["label"],
|
||||
"macro_key": mk,
|
||||
"unit": unit,
|
||||
"coefficient_to_pips": node.get("coefficient_to_pips", 1),
|
||||
"actual_value": _convert(actual_val),
|
||||
"actual_date": actual_date,
|
||||
"forecast_value": _convert(forecast_val),
|
||||
"forecast_date": forecast_date,
|
||||
"next_event_name": next_event_name,
|
||||
"days_until_forecast": days_until,
|
||||
"current_override": overrides.get(node["id"]),
|
||||
})
|
||||
|
||||
return items
|
||||
|
||||
|
||||
# ── Scenario CRUD ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_node_scenarios(conn, instrument: str, node_id: Optional[str] = None) -> list[dict]:
|
||||
|
||||
Reference in New Issue
Block a user