Pular para conteúdo

ADR-003: Ensemble LGBM 0.5 / XGB 0.3 / CAT 0.2

Status: ✅ Accepted (2026-04)

Contexto

Modelos single-algorithm (apenas LightGBM) sofrem com:

  • Bias do algoritmo — LGBM splits via leaf-wise; XGBoost via depth-wise; CatBoost via ordered boosting → cada um pega padrões diferentes
  • Variance dependente do seed — um único modelo é frágil a perturbações no dataset
  • Concept drift — em janelas onde LGBM degrada, outros podem manter robustez
  • Calibração — probabilidades de LGBM puro tendem a ser overconfident em classes minoritárias

Backtest preliminar comparando 3 abordagens em 540d de histórico:

Modelo AUC médio ECE PF Estabilidade (σ)
LGBM puro 0.572 0.31 0.91 0.044
XGB puro 0.561 0.28 0.88 0.052
CAT puro 0.554 0.24 0.85 0.038
Equal-weight (0.33/0.33/0.33) 0.578 0.26 0.96 0.029
Custom (0.5/0.3/0.2) 0.583 0.25 1.02 0.026

Decisão

Adotar ensemble weighted com pesos:

# configs/model.yaml
training:
  use_ensemble: true
  ensemble_weight_lgbm: 0.50
  ensemble_weight_xgb:  0.30
  ensemble_weight_cat:  0.20

Implementação: app/models/predict.py calcula proba = 0.5*p_lgbm + 0.3*p_xgb + 0.2*p_cat por classe, normaliza para soma=1.

Justificativa dos pesos específicos

Por que LGBM = 0.50 (peso maior): - Treina 3-5× mais rápido que XGBoost e CatBoost no nosso dataset (50k bars × 60 features) - Maior AUC individual no histórico (0.572 vs 0.561 vs 0.554) - Tooling maduro (early stopping, categorical features, feature importance via SHAP) - Já era o modelo "champion" anterior — pesos respeitam histórico

Por que XGB = 0.30: - ECE menor que LGBM (0.28 vs 0.31) — ajuda calibração - Suporta missing values nativamente (importante para FDA features) - Diversifica via depth-wise splits

Por que CAT = 0.20: - Melhor calibração individual (ECE 0.24, menor de todos) - Ordered boosting reduz target leakage - Penalizado por ser mais lento e ter AUC menor → peso 0.20 é "voto minoritário" - Útil principalmente como regularizador do ensemble

Alternativas consideradas

1. Apenas LightGBM (mais simples)

Vantagens: - Treina 3× mais rápido - Pipeline mais simples (1 modelo) - Menos código pra manter

Desvantagens: - AUC 0.572 < 0.583 do ensemble (~1% absoluto) - ECE 0.31 > 0.25 — calibração pior - PF 0.91 < 1.02 — backtest perdedor

Decisão: rejeitado. O ganho do ensemble (PF +12%) compensa o custo de treino.

2. Equal-weight ensemble (0.33/0.33/0.33)

Vantagens: - Mais simples conceitualmente (sem hiperparâmetro de peso) - Cada modelo "vota" igual

Desvantagens: - Trata CAT (AUC 0.554) com mesmo peso que LGBM (AUC 0.572) - Ignora diferenças de qualidade individual entre modelos - AUC 0.578 < 0.583 do weighted

Decisão: rejeitado. Diferenciação por qualidade individual rende +0.5% AUC.

3. Stacking / meta-learner

Vantagens: - Aprende pesos ótimos automaticamente - Pode pegar interações não-lineares entre os 3 modelos

Desvantagens: - Adiciona 4º modelo (meta-learner) — risco de overfit - Triplica tempo de treino (precisa validação extra) - Black-box adicional difícil de auditar

Decisão: adiado. Considerar v2.x quando tivermos >100 modelos e estabilidade do ensemble atual estiver comprovada.

4. Bayesian optimization dos pesos por símbolo/TF

Vantagens: - Pesos otimizados por par/timeframe (mais granular) - Pode dar pesos negativos (downvote modelo ruim)

Desvantagens: - 56 conjuntos de pesos para manter (14 símbolos × 4 TFs) - Risk de overfit aos folds de validação - Pesos podem oscilar entre re-treinos semanais

Decisão: adiado. Pesos globais funcionam bem o suficiente.

Consequências

✅ Positivas

  • AUC global +1.1% (0.572 → 0.583)
  • PF backtest +12% (0.91 → 1.02) — modelos passam de perdedores marginais a lucrativos
  • Estabilidade (std AUC) -40% (0.044 → 0.026) — menos variância entre re-treinos
  • Calibração melhor — ECE 0.25 satisfaz quality gate (≤0.28)
  • Resiliência — degradação de 1 modelo (drift) não derruba o ensemble

❌ Negativas

  • Tempo de treino 3× maior (~12 min → ~35 min por slot)
  • Disco 3× maior (3 PKLs por slot — ~30MB total)
  • Complexidade de SHAP — soft voting requer agregar SHAP values dos 3 modelos
  • Hyperparameter tuning 3× — Optuna roda em cada algoritmo

🎯 Trade-offs aceitos

Custo Benefício
+25 min de treino por slot +12% no PF (backtest passa a ser lucrativo)
+20MB por slot em disco -40% de variância (modelos mais estáveis entre re-treinos)
Código 30% maior em predict.py Bypass automático se algum modelo falhar

Robustez do design

Failover graceful:

# app/models/predict.py (pseudo)
predictions = []
for algo in ['lgbm', 'xgb', 'cat']:
    try:
        p = model[algo].predict_proba(X)
        predictions.append((p, weights[algo]))
    except Exception:
        log.warning(f"Modelo {algo} falhou — ensemble continuará sem ele")
        continue

# Renormaliza pesos restantes
total_w = sum(w for _, w in predictions)
proba = sum(p * w / total_w for p, w in predictions)

Se LGBM falhar, ensemble continua com XGB+CAT renormalizados (0.6/0.4). Se 2 falharem, usa o restante. Se todos falharem → NO_TRADE.

Validação contínua

  • Quality gate min_auc_p5: 0.55 exige que percentil 5 dos folds CPCV ainda esteja acima de 0.55 — força o ensemble a ser robusto em todos os cenários
  • Re-screen semanal (scripts/rescreen_champions.py) testa cada modelo isoladamente vs ensemble — se ensemble não supera componentes individuais, modelo é demovido
  • Monitoring de drift por modelo individual em app/monitoring/drift.py — auto-rollback CUSUM se algum algoritmo degrada

Referências

  • Dietterich, T. (2000). Ensemble Methods in Machine Learning. International Workshop on Multiple Classifier Systems
  • Lopez de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. Cap. 7 — "Cross-Validation in Finance"
  • Prokhorenkova, L. et al. (2018). CatBoost: unbiased boosting with categorical features. NeurIPS 2018
  • Ke, G. et al. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. NeurIPS 2017