Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


# =============================================================================
# 1. CONFIGURATION & STYLE
# CONFIGURATION & STYLE
# =============================================================================
st.set_page_config(
page_title="AlphaEdge Dashboard",
Expand All @@ -30,8 +30,6 @@

BASE_DIR = Path(__file__).resolve().parent
HF_REPO_ID = os.getenv("HF_REPO_ID", "soradata/alphaedge-data")

# --- MLflow : mêmes réglages que train.py ---
MLFLOW_TRACKING_URI = "https://soradata-alphaedge-registry.hf.space"
HF_TOKEN = os.getenv("HF_TOKEN")
MLFLOW_ENABLED = bool(HF_TOKEN)
Expand All @@ -40,11 +38,7 @@
os.environ["MLFLOW_TRACKING_USERNAME"] = "SORADATA"
os.environ["MLFLOW_TRACKING_PASSWORD"] = HF_TOKEN
mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)

# Où train.py écrit les model_card.json locaux (MODEL_DIR/{market}/model_card.json).
# Ajuste ce chemin si ton const.MODEL_DIR pointe ailleurs sur ce déploiement.
MODEL_DIR = BASE_DIR / "models"

MARKET_OPTIONS = ["CAC40", "BRVM"]

st.markdown("""
Expand Down Expand Up @@ -94,7 +88,7 @@


# =============================================================================
# 2. CHARGEMENT DES DONNÉES DEPUIS HUGGING FACE (par marché)
# CHARGEMENT DES DONNÉES DEPUIS HUGGING FACE (par marché)
# =============================================================================

@st.cache_data(ttl=600, show_spinner=False)
Expand Down Expand Up @@ -138,7 +132,7 @@ def load_all_data(market: str):


# =============================================================================
# 3. FONCTIONS UTILITAIRES — KPIs / MARCHÉ
# FONCTIONS UTILITAIRES — KPIs / MARCHÉ
# =============================================================================

def display_kpi_card(label, value, is_percent=True, color_code=False, prefix="", minimal=False):
Expand Down Expand Up @@ -310,7 +304,14 @@ def get_champion_metrics(market: str):
if not df_hist.empty:
last_dt = df_hist.index[-1]
days_old = (datetime.now() - last_dt).days
status_icon, status_text = ("🟢", "● System Online") if days_old == 0 else ("🟡", "○ Data Slightly Old") if days_old <= 3 else ("🔴", "○ Data Outdated")

# Considérer les données de la veille comme "System Online" (Stratégie D-1)
if days_old <= 1:
status_icon, status_text = "🟢", "● System Online"
elif days_old <= 3:
status_icon, status_text = "🟡", "○ Data Slightly Old"
else:
status_icon, status_text = "🔴", "○ Data Outdated"
st.sidebar.info(f"Last Update: {last_dt.date()}")
st.sidebar.markdown(f"{status_icon} {status_text}")
else:
Expand All @@ -320,7 +321,7 @@ def get_champion_metrics(market: str):

ticker_val_path = BASE_DIR / "config" / selected_market / "ticker_validation.json"
if not ticker_val_path.exists():
ticker_val_path = BASE_DIR / "ticker_validation.json" # fallback ancien chemin unique
ticker_val_path = BASE_DIR / "ticker_validation.json"
if ticker_val_path.exists():
with st.sidebar.expander("🔍 Ticker Health", expanded=False):
try:
Expand Down Expand Up @@ -657,4 +658,4 @@ def get_champion_metrics(market: str):
<p><strong>Risques :</strong> Tout investissement comporte des risques. Les performances passées ne garantissent pas les résultats futurs.</p>
<p><strong>Responsabilité :</strong> Consultez un conseiller financier agréé avant toute décision d'investissement.</p>
</div>
""", unsafe_allow_html=True)
""", unsafe_allow_html=True)
3 changes: 2 additions & 1 deletion config/markets/cac40.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"market_name": "CAC40",
"benchmark_ticker": "^FCHI",
"tickers": [
"AI.PA", "AIR.PA", "ALO.PA", "MT.AS", "ATO.PA", "CS.PA", "BNP.PA",
"EN.PA", "CAP.PA", "CA.PA", "DSY.PA", "EL.PA", "ENGI.PA", "ERF.PA",
Expand All @@ -9,4 +10,4 @@
"URW.PA", "VIE.PA", "DG.PA", "VIV.PA", "WLN.PA", "FR.PA"
],
"ff_region": "Europe_5_Factors"
}
}
145 changes: 81 additions & 64 deletions src/features/alpha_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
logger = setup_logger("alpha_features")

# ══════════════════════════════════════════════════════════════════
# UTILITAIRES
# 1. SOUS-FONCTIONS MATHÉMATIQUES (Utils)
# ══════════════════════════════════════════════════════════════════

def _safe_div(a: pd.Series, b: pd.Series) -> pd.Series:
Expand All @@ -27,8 +27,7 @@ def _safe_div(a: pd.Series, b: pd.Series) -> pd.Series:
def _rolling_sortino(returns: pd.Series, window: int = 6) -> pd.Series:
def _sortino_scalar(r: np.ndarray) -> float:
neg = r[r < 0]
if len(neg) == 0 or np.std(neg) == 0:
return np.nan
if len(neg) == 0 or np.std(neg) == 0: return np.nan
return (np.mean(r) / np.std(neg)) * np.sqrt(12)
return returns.rolling(window, min_periods=window // 2).apply(_sortino_scalar, raw=True)

Expand All @@ -39,53 +38,100 @@ def _mdd(r: np.ndarray) -> float:
return ((cumulative - peak) / peak).min()
return returns.rolling(window, min_periods=window // 2).apply(_mdd, raw=True)

# ══════════════════════════════════════════════════════════════════
# 2. CALCULS DES FEATURES (Logique métier)
# ══════════════════════════════════════════════════════════════════

def _add_momentum_factors(df: pd.DataFrame, g) -> pd.DataFrame:
df["return_1m"] = g["adj close"].transform(lambda x: x.pct_change(1))
for lag in [2, 3, 6, 9, 12]:
df[f"return_{lag}m"] = g["adj close"].transform(lambda x: x.pct_change(lag))
pct_12 = g["adj close"].transform(lambda x: x.pct_change(12))
pct_1 = g["adj close"].transform(lambda x: x.pct_change(1))
pct_6 = g["adj close"].transform(lambda x: x.pct_change(6))
df["mom_12_1"] = pct_12.div(pct_1.replace(0, np.nan)).replace([np.inf, -np.inf], np.nan)
df["mom_6_1"] = pct_6.div(pct_1.replace(0, np.nan)).replace([np.inf, -np.inf], np.nan)
df["mom_3_1"] = g["adj close"].transform(lambda x: x.pct_change(3))
return df

def _add_mean_reversion_factors(df: pd.DataFrame, g) -> pd.DataFrame:
ma12 = g["adj close"].transform(lambda x: x.rolling(12, min_periods=6).mean())
std12 = g["adj close"].transform(lambda x: x.rolling(12, min_periods=6).std())
df["price_zscore_12"] = _safe_div(df["adj close"] - ma12, std12)
high52 = g["adj close"].transform(lambda x: x.rolling(12, min_periods=6).max())
df["nearness_52w_high"] = _safe_div(df["adj close"], high52)
return df

def _add_volatility_factors(df: pd.DataFrame, g) -> pd.DataFrame:
df["realized_vol_3m"] = g["return_1m"].transform(lambda x: x.rolling(3, min_periods=2).std() * np.sqrt(12))
df["realized_vol_12m"] = g["return_1m"].transform(lambda x: x.rolling(12, min_periods=6).std() * np.sqrt(12))
df["vol_ratio"] = df["realized_vol_3m"].div(df["realized_vol_12m"]).replace([np.inf, -np.inf], np.nan)
excess = df["return_1m"] - df["Mkt-RF"].fillna(0) if "Mkt-RF" in df.columns else df["return_1m"]
df["idio_vol"] = excess.groupby(level=1).transform(lambda x: x.rolling(6, min_periods=3).std() * np.sqrt(12))
return df

def _add_risk_adjusted_factors(df: pd.DataFrame, g) -> pd.DataFrame:
df["sharpe_3m"] = _safe_div(g["return_1m"].transform(lambda x: x.rolling(3, min_periods=2).mean()), g["return_1m"].transform(lambda x: x.rolling(3, min_periods=2).std())) * np.sqrt(12)
df["sharpe_6m"] = _safe_div(g["return_1m"].transform(lambda x: x.rolling(6, min_periods=3).mean()), g["return_1m"].transform(lambda x: x.rolling(6, min_periods=3).std())) * np.sqrt(12)
df["sortino_6m"] = g["return_1m"].transform(lambda x: _rolling_sortino(x, window=6))
df["calmar_proxy"] = _safe_div(g["return_1m"].transform(lambda x: x.rolling(12, min_periods=6).mean() * 12), g["return_1m"].transform(lambda x: _rolling_maxdrawdown(x, window=12)).abs())
return df

def _add_tail_risk_factors(df: pd.DataFrame, g) -> pd.DataFrame:
df["return_skew_6m"] = g["return_1m"].transform(lambda x: x.rolling(6, min_periods=3).skew())
df["return_kurt_6m"] = g["return_1m"].transform(lambda x: x.rolling(6, min_periods=3).kurt())
df["hist_var_5pct"] = g["return_1m"].transform(lambda x: x.rolling(12, min_periods=6).quantile(0.05))
def _cvar(r: np.ndarray) -> float:
t = np.quantile(r, 0.05); tail = r[r <= t]
return tail.mean() if len(tail) > 0 else np.nan
df["cvar_5pct"] = g["return_1m"].transform(lambda x: x.rolling(12, min_periods=6).apply(_cvar, raw=True))
return df

def _add_technical_enrichment(df: pd.DataFrame, g) -> pd.DataFrame:
if "rsi" in df.columns:
df["rsi_divergence"] = g["adj close"].transform(lambda x: x.pct_change(3)) - g["rsi"].transform(lambda x: x.pct_change(3))
if "euro_volume" in df.columns:
df["amihud_illiquidity"] = _safe_div(df["return_1m"].abs(), df["euro_volume"])
df["volume_trend_3m"] = g["euro_volume"].transform(lambda x: x.pct_change(3))
df["volume_zscore"] = g["euro_volume"].transform(lambda x: _safe_div(x - x.rolling(12, min_periods=6).mean(), x.rolling(12, min_periods=6).std()))
return df

def _add_seasonality_features(df: pd.DataFrame) -> pd.DataFrame:
dates = df.index.get_level_values("date")
df["month_sin"] = np.sin(2 * np.pi * dates.month / 12)
df["month_cos"] = np.cos(2 * np.pi * dates.month / 12)
df["is_q_end"] = dates.month.isin([3, 6, 9, 12]).astype(int)
return df

def add_rank_features(df: pd.DataFrame) -> pd.DataFrame:
features_to_rank = [
"mom_12_1", "mom_6_1", "sharpe_6m", "sortino_6m",
"realized_vol_3m", "realized_vol_12m",
"amihud_illiquidity", "return_skew_6m", "hist_var_5pct",
]
features_to_rank = ["mom_12_1", "mom_6_1", "sharpe_6m", "sortino_6m", "realized_vol_3m", "realized_vol_12m", "amihud_illiquidity", "return_skew_6m", "hist_var_5pct"]
for feat in features_to_rank:
rank_col = f"{feat}_rank"
if feat in df.columns:
df[rank_col] = df.groupby(level="date")[feat].transform(lambda x: x.rank(pct=True))
else:
df[rank_col] = 0.5
return df.fillna({f"{f}_rank": 0.5 for f in features_to_rank})
if feat in df.columns: df[f"{feat}_rank"] = df.groupby(level="date")[feat].transform(lambda x: x.rank(pct=True))
else: df[f"{feat}_rank"] = 0.5
return df

# ══════════════════════════════════════════════════════════════════
# 1. CALCULS TECHNIQUES & FACTEURS
# 3. FONCTIONS PRINCIPALES (Exposées)
# ══════════════════════════════════════════════════════════════════

def compute_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
logger.info("Computing daily technical indicators...")
if all(col in df.columns for col in ["high", "low", "open", "adj close"]):
df["garman_klass_vol"] = (
(np.log(df["high"]) - np.log(df["low"])) ** 2 / 2
- (2 * np.log(2) - 1) * (np.log(df["adj close"]) - np.log(df["open"])) ** 2
)
df["garman_klass_vol"] = ((np.log(df["high"]) - np.log(df["low"])) ** 2 / 2 - (2 * np.log(2) - 1) * (np.log(df["adj close"]) - np.log(df["open"])) ** 2)
else:
df["garman_klass_vol"] = 0.0

for ticker in df.index.get_level_values(1).unique():
idx = (slice(None), ticker)
close = df.loc[idx, "adj close"]
if len(close) > MIN_HISTORY_TA:
df.loc[idx, "rsi"] = RSIIndicator(close=close, window=RSI_WINDOW).rsi().values
bb = BollingerBands(close=np.log1p(close), window=BB_WINDOW, window_dev=BB_STD)
df.loc[idx, "bb_low"] = bb.bollinger_lband().values
df.loc[idx, "bb_mid"] = bb.bollinger_mavg().values
df.loc[idx, "bb_high"] = bb.bollinger_hband().values
df.loc[idx, "bb_low"], df.loc[idx, "bb_mid"], df.loc[idx, "bb_high"] = bb.bollinger_lband().values, bb.bollinger_mavg().values, bb.bollinger_hband().values
df.loc[idx, "bb_position"] = ((np.log1p(close) - bb.bollinger_lband()) / (bb.bollinger_hband() - bb.bollinger_lband() + 1e-9))

df["atr"] = df.groupby(level=1, group_keys=False).apply(compute_atr)
df["macd"] = df.groupby(level=1, group_keys=False).apply(compute_macd)
df["macd_sign"] = np.sign(df["macd"].fillna(0))

if "volume" in df.columns:
df["euro_volume"] = (df["adj close"] * df["volume"]) / 1e6
else:
df["euro_volume"] = 0.0
df["euro_volume"] = (df["adj close"] * df["volume"]) / 1e6 if "volume" in df.columns else 0.0
return df

def get_fama_french_betas(data: pd.DataFrame) -> pd.DataFrame:
Expand All @@ -94,14 +140,10 @@ def get_fama_french_betas(data: pd.DataFrame) -> pd.DataFrame:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*date_parser.*")
factor_data = web.DataReader("Europe_5_Factors", "famafrench", start="2010")[0].drop("RF", axis=1)

factor_data.index = pd.to_datetime(factor_data.index.to_timestamp()).tz_localize(None)
factor_data = factor_data.resample("BME").last().div(100)
factor_data.index.name = "date"

if "return_1m" not in data.columns:
return data

if "return_1m" not in data.columns: return data
betas_list = []
for ticker in data.index.get_level_values(1).unique():
ticker_data = data.xs(ticker, level=1)
Expand All @@ -110,40 +152,23 @@ def get_fama_french_betas(data: pd.DataFrame) -> pd.DataFrame:
X = factor_data.loc[factor_data.index.intersection(y.index)]
y = y.loc[X.index]
if len(y) <= MIN_HISTORY_FF: continue

with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*divide by zero.*")
params = RollingOLS(y, sm.add_constant(X[FAMA_FRENCH_FACTORS]), window=MIN_HISTORY_FF).fit().params.drop("const", axis=1)
params = RollingOLS(y, sm.add_constant(X[FAMA_FRENCH_FACTORS]), window=MIN_HISTORY_FF).fit().params.drop("const", axis=1)
params["ticker"] = ticker
betas_list.append(params)

if betas_list:
betas_df = pd.concat(betas_list).set_index("ticker", append=True)
data = data.join(betas_df.groupby("ticker").shift())
data[FAMA_FRENCH_FACTORS] = data.groupby(level="ticker", group_keys=False)[FAMA_FRENCH_FACTORS].transform(lambda x: x.fillna(x.mean()))
return data
except Exception as exc:
logger.warning(f"Fama-French retrieval failed ({exc}).")

return data.assign(**{f: 0.0 for f in FAMA_FRENCH_FACTORS})

# ══════════════════════════════════════════════════════════════════
# 2. ORCHESTRATION DES FEATURES
# ══════════════════════════════════════════════════════════════════

def add_all_features(df: pd.DataFrame) -> pd.DataFrame:
if not isinstance(df.index, pd.MultiIndex):
raise ValueError("MultiIndex requis (date, ticker).")

df = df.copy()

# 1. Calcul Fama-French avant tout (Solution au KeyError)
df = get_fama_french_betas(df)

if not isinstance(df.index, pd.MultiIndex): raise ValueError("MultiIndex requis.")
df = get_fama_french_betas(df.copy())
g = df.groupby(level="ticker")
logger.info("Computing alpha features...")

# 2. Autres calculs
df = _add_momentum_factors(df, g)
df = _add_mean_reversion_factors(df, g)
df = _add_volatility_factors(df, g)
Expand All @@ -152,16 +177,8 @@ def add_all_features(df: pd.DataFrame) -> pd.DataFrame:
df = _add_technical_enrichment(df, g)
df = _add_seasonality_features(df)
df = add_rank_features(df)

# 3. Lags
cols_to_lag = ["rsi", "macd", "bb_low", "bb_mid", "bb_high", "atr", "garman_klass_vol", "bb_position", "macd_sign"] + FAMA_FRENCH_FACTORS
for col in cols_to_lag:
if col not in df.columns:
df[col] = 0.0
if col not in df.columns: df[col] = 0.0
df[f"{col}_lag1"] = df.groupby(level="ticker")[col].shift(1)

df = df.fillna(0).replace([np.inf, -np.inf], 0)
logger.info(f"Features prêtes. Shape : {df.shape}")
return df

# Fonctions internes omises pour la concision (utilise les mêmes que ton ancien fichier)
return df.fillna(0).replace([np.inf, -np.inf], 0)
Loading
Loading