diff --git a/backend/services/instrument_models.py b/backend/services/instrument_models.py index 2215cc1..2a2c081 100644 --- a/backend/services/instrument_models.py +++ b/backend/services/instrument_models.py @@ -1085,25 +1085,29 @@ def simulate_timeline( structural_pips = round(float(vals_struct.get(output_id, 0.0)), 1) fundamental_level_base = round(price_intercept + structural_pips * pip_to_price, 6) - # Auto-anchor : caler le niveau fondamental sur le prix réel au début de la fenêtre. - # start_offset = prix_réel_début - niveau_fondamental_machine - # → synthetic_price(t) = prix_réel_début + event_pips(t) * pip_to_price - start_offset = 0.0 + # Guidance EMA : la baseline de la synthétique est l'EMA lissée du prix réel. + # synthetic_price(t) = EMA(t) + event_pips(t) × pip_to_price + # → sans event perturbateur : synthétique colle au lissé historique + # → avec events : déviation proportionnelle à leur contribution + ema_prices: dict[str, float] = {} + last_ema: float = fundamental_level_base # fallback si pas de données prix try: - ph_row = conn.execute( - """SELECT close FROM price_history_cache - WHERE instrument=? AND date>=? ORDER BY date ASC LIMIT 1""", - (inst_upper, str(date_from)) - ).fetchone() - if ph_row is None: - # Weekends/jours fériés : on remonte jusqu'à 7 jours avant - ph_row = conn.execute( - """SELECT close FROM price_history_cache - WHERE instrument=? AND date>=? ORDER BY date ASC LIMIT 1""", - (inst_upper, str(date_from - timedelta(days=7))) - ).fetchone() - if ph_row: - start_offset = round(float(ph_row["close"]) - fundamental_level_base, 6) + # 30j de warmup avant date_from pour que l'EMA soit stabilisée dès le début + warmup_from = str(date_from - timedelta(days=30)) + ph_rows = conn.execute( + """SELECT date, close FROM price_history_cache + WHERE instrument=? AND date>=? ORDER BY date ASC""", + (inst_upper, warmup_from) + ).fetchall() + alpha = 0.15 # lissage EMA (~6j de demi-vie) + ema_val: Optional[float] = None + for r in ph_rows: + c = float(r["close"]) + ema_val = c if ema_val is None else alpha * c + (1.0 - alpha) * ema_val + if r["date"] >= str(date_from): + ema_prices[r["date"]] = round(ema_val, 6) + if ema_prices: + last_ema = list(ema_prices.values())[-1] except Exception: pass @@ -1150,13 +1154,23 @@ def simulate_timeline( net = structural_pips regime_label = "BALANCED" + # Guide price : EMA du prix réel si disponible, sinon dernier EMA connu (futur) + date_str = str(cur) + if date_str in ema_prices: + guide_price = ema_prices[date_str] + last_ema = guide_price + else: + guide_price = last_ema # dates futures : tient le dernier EMA connu + + event_pips = round(net - structural_pips, 1) + timeline.append({ - "date": str(cur), + "date": date_str, "net_pips": net, "structural_pips": structural_pips, - "event_pips": round(net - structural_pips, 1), - "fundamental_level": round(fundamental_level_base + start_offset, 6), - "synthetic_price": round(price_intercept + start_offset + net * pip_to_price, 6), + "event_pips": event_pips, + "fundamental_level": guide_price, + "synthetic_price": round(guide_price + event_pips * pip_to_price, 6), "regime": regime_label, "nodes": {k: round(float(v), 1) for k, v in vals.items()}, "active_events": active_events_detail,