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.55exige 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