Skip to content

ADR-46 — Class balancing obligatoire en training binaire

Status: active (backfilled 2026-04-24 from prior references) Date: 2026-04-24 (backfill) · originally implied by code changes around Sprint 10–11 Introduced by: backfill under #637 (Phase 2 of #593) Supersedes: —


Context

En mission F1=0.75 binaire, la distribution empirique des labels triple-barrier sur le timeframe 15m / H4 est approximativement 70 % HOLD · 15 % BUY · 15 % SELL (cf. documentation/architecture/TRAINING_PIPELINE.md §Problem Diagnosis). Sans rééquilibrage, le modèle apprend à prédire HOLD par défaut (minimise la cross-entropy globale) et sous-pondère la classe BUY.

Résultats observés sans class balancing (CVN_CLASS_BALANCING=0) : - BUY recall ≈ 0.25 — 75 % des opportunités ratées - f1_buy collapse vers le baseline naïf (const_F1) - Probabilités non calibrées pour le downstream (threshold calibrator, meta-label)

ADR-28 fixe le mode binaire ; ADR-29 impose la baseline naïve ; cet ADR complète les deux en rendant explicite l'obligation du rééquilibrage.

Decision

Le flag CVN_CLASS_BALANCING vaut 1 dans la base_env FTF (PostgreSQL ftf_config + config/ftf_baseline.json + Helm values-prod.yaml) — c'est la valeur effective dans tous les environnements de production, training, et tuning. L'implémentation utilise sklearn.utils.class_weight.compute_class_weight(class_weight="balanced", ...) puis passe les poids au trainer (XGBoost sample_weight, LightGBM sample_weight, CatBoost class_weights).

Note sur le défaut Python : _is_class_balancing_enabled() lit os.getenv("CVN_CLASS_BALANCING", "0") (src/training/XGBoost/cvntrade_XGBoost_trainer.py:20) — le fallback "0" n'est PAS le défaut opérationnel ; c'est un fail-safe : si un trainer s'exécute hors du contexte FTF/Airflow (où base_env est injecté), il tourne en mode plus sûr (pas de poids de classe non-désirés sur un dataset arbitraire). En production, CVN_CLASS_BALANCING=1 est toujours injecté en amont.

Opt-out explicite uniquement via factor FTF class_balancing avec la variante OFF (cf. src/commun/finetune/ablation_matrix.py:338) — pour A/B testing en ablation, jamais comme baseline.

Invariants

  • Invariant 1 : base_env["CVN_CLASS_BALANCING"] == "1" dans ftf_config ET dans config/ftf_baseline.json ET dans infra/helm/airflow/values-prod.yaml — divergence entre ces trois sources viole ADR-46.
  • Invariant 2 : les trois trainers (XGBoost, LightGBM, CatBoost) consomment les poids de classe quand CVN_CLASS_BALANCING=1. Un trainer qui ignore silencieusement le flag viole ADR-25 (pas de fallback silencieux).
  • Invariant 3 : tout run avec CVN_CLASS_BALANCING=0 DOIT être tagué class_balancing=disabled dans MLflow + ne PEUT pas être promu en production (ADR-42).

Alternatives rejected

  • Oversampling SMOTE : augmente la variance sur les labels triple-barrier (corrélés temporellement). Rejetée — sklearn class_weight est moins invasif et préserve la distribution temporelle.
  • Focal loss : change la surface d'optimisation — non supporté nativement par XGBoost/LightGBM/CatBoost en binaire, nécessite custom objective. Gardé en backlog si class_weight insuffisant.
  • Sur-échantillonnage de la classe minoritaire : biais le train set, fausse la perplexité HPO. Rejetée pour les mêmes raisons que SMOTE.
  • Laisser le modèle apprendre la distribution native : cf. Context — f1_buy ≈ const_F1, mission impossible.

Consequences

  • Positive : BUY recall remonte (+0.10 f1_buy mesuré sur Fix B du Sprint 11 diagnostic), probabilités mieux étalées → seuil calibré plus robuste.
  • Negative : trois trainers à maintenir en parité sur le flag. Un nouveau trainer doit implémenter l'injection du sample_weight équilibré pour être éligible à la FTF (gardé par un guardrail à venir).
  • Neutral : le choix balanced par scikit-learn assume une distribution équilibrée post-poids ; d'autres schémas (carrés inverses, effective number of samples) restent explorables via un futur factor.

Rollback

  • Toggle CVN_CLASS_BALANCING=0 dans ftf_config via Console (ADR-59).
  • Ou variant FTF class_balancing=disabled pour ablation ponctuelle.
  • Aucune purge de cache requise ; l'impact est sur la prochaine run.

References

  • Parent need: CVN-N001 (F1 mission, #608)
  • Related ADRs: ADR-25, ADR-28, ADR-29, ADR-42, ADR-59
  • Code: src/training/XGBoost/cvntrade_XGBoost_trainer.py, src/commun/finetune/ablation_matrix.py (factor class_balancing)
  • Docs: documentation/architecture/TRAINING_PIPELINE.md §Class balancing