feat: instrument model
This commit is contained in:
@@ -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