xauusd_bot/macro_features.py

313 lines
12 KiB
Python

# ============================================================
# macro_features.py — Features Macro : DXY, Taux 10 ans, Sessions
# ============================================================
# Données récupérées via yfinance (Yahoo Finance) — gratuit, sans API key
# Mise à jour automatique toutes les heures en live
# ============================================================
import logging
import threading
import time
import numpy as np
import pandas as pd
from datetime import datetime, timezone
from typing import Dict, Optional, Tuple
import pytz
logger = logging.getLogger(__name__)
class MacroFeaturesModule:
"""
Récupère et met à jour les données macro en temps réel :
1. DXY (Dollar Index) — Corrélation -0.85 avec l'or
2. US10Y (Taux 10 ans USA) — Taux réels vs or
3. Session de trading — Londres/NY/Asie/Hors-session
4. Jour de la semaine — Patterns hebdomadaires
"""
def __init__(self):
self._data: Dict = {
# DXY
"dxy_price": 100.0, # Prix actuel DXY
"dxy_return_1d": 0.0, # Variation journalière DXY
"dxy_return_5d": 0.0, # Variation 5 jours DXY
"dxy_vs_sma20": 0.0, # DXY au-dessus/dessous SMA20
"dxy_rsi": 50.0, # RSI du DXY
# Taux 10 ans US
"us10y_rate": 4.0, # Taux en %
"us10y_change_1d": 0.0, # Variation journalière en bps
"us10y_change_5d": 0.0, # Variation 5 jours
"real_rate_proxy": 0.0, # Taux 10 ans - inflation proxy
# Session de trading
"session_asia": 0, # 1 si session Asie active
"session_london": 0, # 1 si session Londres active
"session_newyork": 0, # 1 si session New York active
"session_overlap": 0, # 1 si chevauchement Londres/NY
"hour_sin": 0.0, # Heure encodée cycliquement (sin)
"hour_cos": 1.0, # Heure encodée cycliquement (cos)
# Jour de la semaine
"day_monday": 0,
"day_tuesday": 0,
"day_wednesday": 0, # Souvent FOMC
"day_thursday": 0,
"day_friday": 0, # NFP + clôture positions
"day_sin": 0.0, # Jour encodé cycliquement
"day_cos": 1.0,
# Métadonnées
"last_update": None,
"data_available": False,
}
self._lock = threading.Lock()
self._running = False
self._thread: Optional[threading.Thread] = None
# ── API Publique ───────────────────────────────────────────
def start(self):
"""Lance la mise à jour en arrière-plan (toutes les heures)."""
self._running = True
# Première mise à jour synchrone
self._update_all()
# Puis thread en arrière-plan
self._thread = threading.Thread(target=self._update_loop, daemon=True)
self._thread.start()
logger.info("MacroFeatures : module démarré.")
def stop(self):
self._running = False
if self._thread:
self._thread.join(timeout=5)
def get_features(self) -> Dict:
"""Retourne toutes les features macro (thread-safe)."""
with self._lock:
# Toujours mettre à jour les features temps-réel (session/heure)
self._update_time_features_inplace()
return dict(self._data)
def get_feature_vector(self) -> np.ndarray:
"""
Retourne un vecteur numpy normalisé des features macro.
Utilisé directement comme input supplémentaire du réseau IA.
"""
d = self.get_features()
vector = np.array([
# DXY (normalisé autour de 0)
np.clip(d["dxy_return_1d"] * 100, -3, 3), # % var jour
np.clip(d["dxy_return_5d"] * 100, -5, 5), # % var 5j
np.clip(d["dxy_vs_sma20"] * 100, -5, 5), # vs SMA20
np.clip((d["dxy_rsi"] - 50) / 50, -1, 1), # RSI centré
# Taux 10 ans (normalisé)
np.clip(d["us10y_rate"] / 10, 0, 1), # Niveau absolu
np.clip(d["us10y_change_1d"] / 20, -1, 1), # Variation bps/j
np.clip(d["us10y_change_5d"] / 50, -1, 1), # Variation bps/5j
np.clip(d["real_rate_proxy"] / 5, -1, 1), # Taux réels proxy
# Sessions (binaires)
float(d["session_asia"]),
float(d["session_london"]),
float(d["session_newyork"]),
float(d["session_overlap"]),
# Encodage temporel cyclique
d["hour_sin"],
d["hour_cos"],
d["day_sin"],
d["day_cos"],
# Jours spéciaux
float(d["day_wednesday"]), # FOMC souvent mercredi
float(d["day_friday"]), # NFP + clôture
], dtype=np.float32)
return np.clip(vector, -3, 3)
def get_feature_names(self) -> list:
"""Noms des features dans l'ordre du vecteur."""
return [
"dxy_return_1d", "dxy_return_5d", "dxy_vs_sma20", "dxy_rsi",
"us10y_rate", "us10y_change_1d", "us10y_change_5d", "real_rate_proxy",
"session_asia", "session_london", "session_newyork", "session_overlap",
"hour_sin", "hour_cos", "day_sin", "day_cos",
"day_wednesday", "day_friday",
]
# ── Mises à jour ───────────────────────────────────────────
def _update_loop(self):
"""Met à jour les données toutes les heures."""
while self._running:
time.sleep(3600) # 1 heure
try:
self._update_all()
except Exception as e:
logger.error(f"MacroFeatures update error: {e}")
def _update_all(self):
"""Récupère DXY + Taux 10 ans via yfinance."""
try:
import yfinance as yf
except ImportError:
logger.warning("yfinance non installé. pip install yfinance")
logger.warning("Utilisation des valeurs par défaut pour les features macro.")
with self._lock:
self._update_time_features_inplace()
return
dxy_ok = self._fetch_dxy(yf)
rates_ok = self._fetch_us10y(yf)
with self._lock:
self._update_time_features_inplace()
self._data["data_available"] = dxy_ok or rates_ok
self._data["last_update"] = datetime.utcnow()
logger.info(
f"MacroFeatures mis a jour | "
f"DXY={self._data['dxy_price']:.2f} | "
f"US10Y={self._data['us10y_rate']:.2f}% | "
f"Session={self._get_session_name()}"
)
def _fetch_dxy(self, yf) -> bool:
"""Récupère les données du Dollar Index (DX-Y.NYB)."""
try:
ticker = yf.Ticker("DX-Y.NYB")
hist = ticker.history(period="30d", interval="1d")
if hist.empty or len(hist) < 5:
logger.warning("DXY: données insuffisantes")
return False
closes = hist["Close"].values
price = float(closes[-1])
sma20 = float(np.mean(closes[-20:])) if len(closes) >= 20 else price
ret_1d = (closes[-1] / closes[-2] - 1) if len(closes) >= 2 else 0.0
ret_5d = (closes[-1] / closes[-5] - 1) if len(closes) >= 5 else 0.0
# RSI du DXY
delta = np.diff(closes[-15:])
gain = np.mean(delta[delta > 0]) if any(delta > 0) else 0
loss = abs(np.mean(delta[delta < 0])) if any(delta < 0) else 1e-9
rsi = 100 - (100 / (1 + gain / loss))
with self._lock:
self._data.update({
"dxy_price": price,
"dxy_return_1d": float(ret_1d),
"dxy_return_5d": float(ret_5d),
"dxy_vs_sma20": float((price - sma20) / sma20),
"dxy_rsi": float(rsi),
})
return True
except Exception as e:
logger.debug(f"DXY fetch error: {e}")
return False
def _fetch_us10y(self, yf) -> bool:
"""Récupère le taux des obligations US 10 ans (^TNX)."""
try:
ticker = yf.Ticker("^TNX")
hist = ticker.history(period="30d", interval="1d")
if hist.empty or len(hist) < 2:
logger.warning("US10Y: données insuffisantes")
return False
closes = hist["Close"].values
rate = float(closes[-1]) # En %
change_1d = float(closes[-1] - closes[-2]) * 100 # En bps
change_5d = float(closes[-1] - closes[-5]) * 100 if len(closes) >= 5 else 0.0
# Proxy taux réels = taux 10 ans - inflation estimée (CPI ~3%)
inflation_proxy = 3.0
real_rate = rate - inflation_proxy
with self._lock:
self._data.update({
"us10y_rate": rate,
"us10y_change_1d": change_1d,
"us10y_change_5d": change_5d,
"real_rate_proxy": real_rate,
})
return True
except Exception as e:
logger.debug(f"US10Y fetch error: {e}")
return False
def _update_time_features_inplace(self):
"""Met à jour les features temporelles (appelé sans lock)."""
now_utc = datetime.now(timezone.utc)
hour = now_utc.hour + now_utc.minute / 60.0
weekday = now_utc.weekday() # 0=lundi, 6=dimanche
# ── Sessions de trading (heures UTC) ──────────────────
# Asie : 00:00 - 08:00 UTC
# Londres : 08:00 - 17:00 UTC
# New York: 13:00 - 22:00 UTC
# Overlap : 13:00 - 17:00 UTC (Londres + NY)
asia_open = 0.0 <= hour < 8.0
london_open = 8.0 <= hour < 17.0
ny_open = 13.0 <= hour < 22.0
overlap = 13.0 <= hour < 17.0
# Week-end = marchés fermés
is_weekend = weekday >= 5
if is_weekend:
asia_open = london_open = ny_open = overlap = False
# ── Encodage cyclique heure (sin/cos) ──────────────────
hour_rad = (hour / 24.0) * 2 * np.pi
hour_sin = float(np.sin(hour_rad))
hour_cos = float(np.cos(hour_rad))
# ── Encodage cyclique jour semaine ─────────────────────
day_rad = (weekday / 7.0) * 2 * np.pi
day_sin = float(np.sin(day_rad))
day_cos = float(np.cos(day_rad))
self._data.update({
"session_asia": int(asia_open),
"session_london": int(london_open),
"session_newyork": int(ny_open),
"session_overlap": int(overlap),
"hour_sin": hour_sin,
"hour_cos": hour_cos,
"day_monday": int(weekday == 0),
"day_tuesday": int(weekday == 1),
"day_wednesday": int(weekday == 2),
"day_thursday": int(weekday == 3),
"day_friday": int(weekday == 4),
"day_sin": day_sin,
"day_cos": day_cos,
})
def _get_session_name(self) -> str:
"""Retourne le nom de la session active."""
d = self._data
if d["session_overlap"]: return "OVERLAP London/NY"
if d["session_newyork"]: return "NEW YORK"
if d["session_london"]: return "LONDRES"
if d["session_asia"]: return "ASIE"
return "HORS SESSION"
def get_dashboard_string(self) -> str:
"""Résumé pour le dashboard."""
d = self.get_features()
dxy_arrow = "" if d["dxy_return_1d"] > 0 else ""
us10y_arrow = "" if d["us10y_change_1d"] > 0 else ""
return (
f"DXY={d['dxy_price']:.2f}{dxy_arrow} | "
f"US10Y={d['us10y_rate']:.2f}%{us10y_arrow} | "
f"Session={self._get_session_name()}"
)