Ensemble Model Stacking
Build a stacked ensemble architecture that combines predictions from multiple heterogeneous base ML models using a meta-learner, producing more robust and accurate trading signals than any individual model alone.
Ensemble Model Stacking for Trading Signal Generation
Ensemble model stacking is a meta-learning technique in which multiple base learners are trained independently, and their predictions are combined by a higher-level meta-model to produce a final output. In the context of algorithmic trading, stacking leverages the complementary strengths of heterogeneous models — such as gradient boosting, support vector machines, and logistic regression — to generate more robust directional signals than any single model can produce alone. This notebook demonstrates a complete, production-ready stacking pipeline applied to synthetic financial feature data using scikit-learn.
Library Imports
The following libraries are required for data generation, model construction, stacking, evaluation, and visualization. All dependencies are standard components of the scientific Python ecosystem.
# Standard scientific and ML libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import warnings
# Scikit-learn model components
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier, StackingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import TimeSeriesSplit, cross_val_score, KFold
from sklearn.metrics import (
accuracy_score,
classification_report,
roc_auc_score,
confusion_matrix,
ConfusionMatrixDisplay,
roc_curve
)
warnings.filterwarnings("ignore")
np.random.seed(42)Strategy Overview
Problem Statement
In financial machine learning, a single model trained on market features is prone to overfitting and limited generalization across different market regimes. Ensemble stacking addresses this by training multiple diverse base models on the same feature set, then training a meta-model to learn the optimal combination of their outputs.
Approach
The pipeline implemented in this notebook proceeds through the following stages:
-
Synthetic Data Generation: A realistic set of financial features is constructed — including lagged returns, volatility estimates, momentum indicators, and volume proxies — along with a binary directional label derived from forward returns.
-
Feature Engineering: Raw price-derived inputs are transformed into normalized, stationary features suitable for ML consumption.
-
Base Model Definition: Four heterogeneous base learners are defined — Random Forest, Gradient Boosting, Support Vector Machine, and Logistic Regression — each capturing different non-linear and linear patterns in the feature space.
-
Stacking Architecture: A
StackingClassifieris constructed using cross-validated out-of-fold predictions from base models as inputs to a Logistic Regression meta-learner. -
Evaluation: The stacked ensemble is evaluated against each base model individually using accuracy, ROC-AUC, and classification metrics.
-
Signal Generation: The trained ensemble generates probabilistic trading signals on out-of-sample data.
-
Visualization: Model performance comparisons and signal distributions are visualized.
Rationale
Stacking is preferred over simple averaging or voting because the meta-learner can adaptively weight base model contributions, suppress systematic biases, and exploit inter-model correlations that fixed ensemble schemes cannot capture.
Function: generate_synthetic_market_data
This function constructs a synthetic financial dataset that mimics the statistical properties of daily equity or crypto price data. Features are derived from simulated log-returns and include lagged returns, rolling volatility, momentum scores, and volume proxies. The binary target label is defined as 1 when the next-period return exceeds a threshold, and 0 otherwise. This construction ensures temporal ordering is preserved, which is critical for time-series cross-validation.
def generate_synthetic_market_data(n_samples=2000, noise_level=0.3, seed=42):
"""
Generate a synthetic financial feature dataset for binary classification.
Parameters
----------
n_samples : int
Number of time steps (rows) to simulate.
noise_level : float
Standard deviation of added noise to simulate market randomness.
seed : int
Random seed for reproducibility.
Returns
-------
X : pd.DataFrame
Feature matrix with shape (n_samples - window, n_features).
y : pd.Series
Binary directional label: 1 = up, 0 = down/flat.
feature_names : list
List of feature column names.
"""
np.random.seed(seed)
window = 20 # Rolling window size for feature construction
# Simulate log-returns from a slightly drift-adjusted normal distribution
log_returns = np.random.normal(loc=0.0002, scale=0.015, size=n_samples)
# Add regime shifts to introduce autocorrelation structure
regime = np.sin(np.linspace(0, 6 * np.pi, n_samples)) * 0.003
log_returns += regime + np.random.normal(0, noise_level * 0.01, n_samples)
# Reconstruct price series from log-returns
price = 100 * np.exp(np.cumsum(log_returns))
# Simulate volume with correlation to absolute returns (volume-volatility relationship)
volume = np.abs(log_returns) * 1e6 + np.random.lognormal(mean=13, sigma=0.3, size=n_samples)
# --- Feature Construction ---
# Lagged returns: capture short-term momentum/mean-reversion signals
lag1 = pd.Series(log_returns).shift(1)
lag2 = pd.Series(log_returns).shift(2)
lag3 = pd.Series(log_returns).shift(3)
lag5 = pd.Series(log_returns).shift(5)
# Rolling volatility: realized standard deviation over rolling window
rolling_vol = pd.Series(log_returns).rolling(window).std()
# Rolling momentum: cumulative return over the last window periods
rolling_momentum = pd.Series(log_returns).rolling(window).sum()
# Volatility-adjusted momentum (Sharpe-like ratio)
vol_adj_momentum = rolling_momentum / (rolling_vol + 1e-8)
# Volume signal: z-score of volume relative to its rolling mean
vol_series = pd.Series(volume)
volume_zscore = (vol_series - vol_series.rolling(window).mean()) / (vol_series.rolling(window).std() + 1e-8)
# Price distance from rolling mean (mean-reversion indicator)
price_series = pd.Series(price)
price_ma = price_series.rolling(window).mean()
price_distance = (price_series - price_ma) / (price_ma + 1e-8)
# High-frequency proxy: absolute return magnitude
abs_return = np.abs(log_returns)
# Autocorrelation proxy: product of lag-1 and lag-2 returns
return_autocorr = lag1 * lag2
# Assemble feature matrix
feature_dict = {
"lag_return_1": lag1,
"lag_return_2": lag2,
"lag_return_3": lag3,
"lag_return_5": lag5,
"rolling_volatility": rolling_vol,
"rolling_momentum": rolling_momentum,
"vol_adj_momentum": vol_adj_momentum,
"volume_zscore": volume_zscore,
"price_distance_ma": price_distance,
"abs_return": abs_return,
"return_autocorr": return_autocorr,
}
df_features = pd.DataFrame(feature_dict)
# Binary label: 1 if next-period return is positive, 0 otherwise
forward_return = pd.Series(log_returns).shift(-1)
labels = (forward_return > 0).astype(int)
# Combine and drop NaN rows introduced by lagging/rolling
df_all = df_features.copy()
df_all["label"] = labels
df_all.dropna(inplace=True)
df_all.reset_index(drop=True, inplace=True)
X = df_all.drop(columns=["label"])
y = df_all["label"]
feature_names = list(X.columns)
print(f"Dataset shape: {X.shape}")
print(f"Class distribution: {y.value_counts().to_dict()}")
return X, y, feature_namesFunction: split_time_series_data
Financial data has temporal ordering that standard random train-test splits violate, leading to data leakage and overly optimistic performance estimates. This function performs a strict chronological split, assigning the first portion of the data to training and the remainder to out-of-sample testing.
def split_time_series_data(X, y, train_ratio=0.75):
"""
Perform a chronological train-test split preserving temporal ordering.
Parameters
----------
X : pd.DataFrame
Feature matrix.
y : pd.Series
Target labels.
train_ratio : float
Proportion of data allocated to training (default: 0.75).
Returns
-------
X_train, X_test : pd.DataFrame
Training and test feature matrices.
y_train, y_test : pd.Series
Training and test label series.
split_index : int
Row index at which the split occurs.
"""
n = len(X)
split_index = int(n * train_ratio)
# Slice chronologically — no shuffling
X_train = X.iloc[:split_index].copy()
X_test = X.iloc[split_index:].copy()
y_train = y.iloc[:split_index].copy()
y_test = y.iloc[split_index:].copy()
print(f"Training samples : {len(X_train)}")
print(f"Test samples : {len(X_test)}")
print(f"Split index : {split_index}")
return X_train, X_test, y_train, y_test, split_indexFunction: define_base_models
Four base estimators are defined here, each wrapped inside a Pipeline with a StandardScaler to ensure consistent feature scaling across model types. Diversity among base models is a critical prerequisite for effective stacking — models with different inductive biases learn complementary patterns, allowing the meta-learner to exploit their disagreements productively.
- Random Forest: Bagged decision trees; robust to noise and captures non-linear interactions.
- Gradient Boosting: Sequential boosting over residuals; high predictive power on tabular data.
- Support Vector Machine (SVM): Kernel-based margin classifier; effective in high-dimensional spaces.
- Logistic Regression (Base): Linear probabilistic classifier; provides a calibrated baseline.
def define_base_models():
"""
Define and return a list of heterogeneous base estimator pipelines.
Each base model is wrapped in a StandardScaler pipeline to ensure
consistent preprocessing across model types.
Returns
-------
base_models : list of tuples
Each tuple contains (name_str, Pipeline) compatible with StackingClassifier.
"""
# Random Forest: ensemble of decision trees via bagging
rf_pipeline = Pipeline([
("scaler", StandardScaler()),
("model", RandomForestClassifier(
n_estimators=200, # Number of trees
max_depth=6, # Limit depth to reduce overfitting
min_samples_leaf=10, # Regularization: minimum samples per leaf
n_jobs=-1,
random_state=42
))
])
# Gradient Boosting: sequential weak learners optimized via gradient descent
gb_pipeline = Pipeline([
("scaler", StandardScaler()),
("model", GradientBoostingClassifier(
n_estimators=200, # Number of boosting stages
learning_rate=0.05, # Shrinkage factor to prevent overfitting
max_depth=4, # Shallow trees as weak learners
subsample=0.8, # Stochastic gradient boosting
random_state=42
))
])
# Support Vector Machine: RBF kernel for non-linear decision boundary
svm_pipeline = Pipeline([
("scaler", StandardScaler()),
("model", SVC(
kernel="rbf", # Radial basis function kernel
C=1.0, # Regularization strength
gamma="scale", # Kernel coefficient scaled by feature variance
probability=True, # Required for predict_proba in stacking
random_state=42
))
])
# Logistic Regression: linear probabilistic classifier as calibrated baseline
lr_base_pipeline = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(
C=0.1, # Stronger L2 regularization
max_iter=1000,
random_state=42
))
])
# Return as list of (name, estimator) tuples
base_models = [
("random_forest", rf_pipeline),
("gradient_boosting", gb_pipeline),
("svm", svm_pipeline),
("logistic_regression_base", lr_base_pipeline),
]
print(f"Base models defined: {[name for name, _ in base_models]}")
return base_modelsFunction: build_stacking_ensemble
This function constructs the StackingClassifier using the base models defined above. The meta-learner is a Logistic Regression model that receives as input the out-of-fold predictions generated by each base estimator during cross-validation. The passthrough=False setting instructs the meta-learner to use only the base model outputs — not the original features — thereby isolating the meta-learning signal. TimeSeriesSplit is used as the cross-validation strategy to preserve temporal ordering during the stacking fold generation.
def build_stacking_ensemble(base_models, n_splits=5):
"""
Construct a StackingClassifier with a Logistic Regression meta-learner.
The stacking classifier trains base models using time-series cross-validation
to generate out-of-fold predictions, which are then used to train the meta-learner.
Parameters
----------
base_models : list of tuples
List of (name, estimator) pairs defining the base learners.
n_splits : int
Number of folds for TimeSeriesSplit cross-validation (default: 5).
Returns
-------
stacking_clf : StackingClassifier
Compiled stacking ensemble ready for fitting.
"""
# Use KFold with shuffle=False to ensure all samples are covered in test folds
# to resolve 'cross_val_predict only works for partitions' error with StackingClassifier.
# TimeSeriesSplit is fundamentally incompatible with StackingClassifier's internal
# cross_val_predict mechanism, as it doesn't create a full partition of the data.
kf = KFold(n_splits=n_splits, shuffle=False)
# Meta-learner: Logistic Regression with strong regularization
meta_learner = LogisticRegression(
C=0.5,
max_iter=1000,
random_state=42
)
# StackingClassifier: trains base models on CV folds, trains meta-learner on OOF outputs
stacking_clf = StackingClassifier(
estimators=base_models, # Base estimator list
final_estimator=meta_learner, # Meta-model
cv=kf, # Changed from tscv to kf to resolve partition error
stack_method="predict_proba", # Use probability outputs as meta-features
passthrough=False, # Do not include original features in meta-input
n_jobs=1 # Changed from -1 to 1 to address multiprocessing issue
)
print("Stacking ensemble constructed.")
print(f"Meta-learner: {type(meta_learner).__name__}")
print(f"CV strategy : KFold(n_splits={n_splits}, shuffle=False)")
return stacking_clfFunction: train_and_evaluate_models
Each base model and the stacking ensemble are trained on the training set and evaluated on the held-out test set. This function returns a results dictionary containing accuracy, ROC-AUC, predicted labels, and predicted probabilities for downstream analysis and comparison.
def train_and_evaluate_models(base_models, stacking_clf, X_train, X_test, y_train, y_test):
"""
Train all base models and the stacking ensemble, then evaluate each on the test set.
Parameters
----------
base_models : list of tuples
List of (name, estimator) base model pipelines.
stacking_clf : StackingClassifier
The constructed stacking ensemble.
X_train, X_test : pd.DataFrame
Training and test feature matrices.
y_train, y_test : pd.Series
Training and test labels.
Returns
-------
results : dict
Dictionary keyed by model name; each value contains:
- 'model' : fitted estimator object
- 'accuracy' : test accuracy
- 'roc_auc' : test ROC-AUC score
- 'y_pred' : predicted labels
- 'y_proba' : predicted probabilities for class 1
"""
results = {}
# --- Evaluate each base model independently ---
for name, model in base_models:
print(f"\nTraining base model: {name} ...")
model.fit(X_train, y_train)
# Generate predictions on held-out test set
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1] # Probability of class 1 (up)
acc = accuracy_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_proba)
results[name] = {
"model": model,
"accuracy": acc,
"roc_auc": auc,
"y_pred": y_pred,
"y_proba": y_proba,
}
print(f" Accuracy : {acc:.4f}")
print(f" ROC-AUC : {auc:.4f}")
# --- Evaluate stacking ensemble ---
print("\nTraining stacking ensemble ...")
stacking_clf.fit(X_train, y_train)
y_pred_stack = stacking_clf.predict(X_test)
y_proba_stack = stacking_clf.predict_proba(X_test)[:, 1]
acc_stack = accuracy_score(y_test, y_pred_stack)
auc_stack = roc_auc_score(y_test, y_proba_stack)
results["stacking_ensemble"] = {
"model": stacking_clf,
"accuracy": acc_stack,
"roc_auc": auc_stack,
"y_pred": y_pred_stack,
"y_proba": y_proba_stack,
}
print(f"\nStacking Ensemble:")
print(f" Accuracy : {acc_stack:.4f}")
print(f" ROC-AUC : {auc_stack:.4f}")
return resultsFunction: print_classification_reports
Full classification reports (precision, recall, F1-score, support) are printed for each model. These metrics provide signal quality assessment beyond accuracy, particularly for evaluating trade entry precision and directional reliability.
def print_classification_reports(results, y_test):
"""
Print full classification reports for all trained models.
Parameters
----------
results : dict
Output from train_and_evaluate_models.
y_test : pd.Series
True test labels.
"""
for name, data in results.items():
print(f"\n{'='*55}")
print(f"Classification Report --- {name}")
print('='*55)
print(classification_report(
y_test,
data["y_pred"],
target_names=["Down/Flat (0)", "Up (1)"]
))Function: generate_trading_signals
The trained stacking ensemble is applied to the full out-of-sample test set to generate probabilistic trading signals. A configurable confidence threshold filters signals: only predictions where the model's probability exceeds the threshold are acted upon. This thresholding mechanism simulates a real-world signal gate that rejects low-confidence predictions.
def generate_trading_signals(stacking_clf, X_test, threshold=0.55):
"""
Generate filtered trading signals from the stacking ensemble's probability output.
Parameters
----------
stacking_clf : StackingClassifier
Trained stacking ensemble.
X_test : pd.DataFrame
Out-of-sample feature matrix.
threshold : float
Minimum probability required to issue a long signal (default: 0.55).
Returns
-------
signal_df : pd.DataFrame
DataFrame containing raw probabilities, thresholded signals, and signal strength.
"""
# Predicted probability of upward move
prob_up = stacking_clf.predict_proba(X_test)[:, 1]
# Signal strength: distance from 0.5 (neutral boundary)
signal_strength = np.abs(prob_up - 0.5)
# Apply threshold: 1 = long signal, 0 = no signal, -1 = short signal
signal = np.where(prob_up >= threshold, 1,
np.where(prob_up <= (1 - threshold), -1, 0))
signal_df = pd.DataFrame({
"prob_up": prob_up,
"signal": signal,
"signal_strength": signal_strength
}, index=X_test.index)
# Signal distribution summary
long_signals = (signal_df["signal"] == 1).sum()
short_signals = (signal_df["signal"] == -1).sum()
no_signals = (signal_df["signal"] == 0).sum()
print(f"Signal threshold : {threshold}")
print(f"Long signals (1) : {long_signals}")
print(f"Short signals (-1) : {short_signals}")
print(f"No signal (0) : {no_signals}")
print(f"Total bars : {len(signal_df)}")
return signal_dfFunction: compute_cross_val_scores
Time-series cross-validation scores are computed for each base model and the stacking ensemble to assess generalization stability across time segments. High variance in CV scores indicates model instability across market regimes.
def compute_cross_val_scores(base_models, stacking_clf, X_train, y_train, n_splits=5):
"""
Compute time-series cross-validation ROC-AUC scores for all models.
Parameters
----------
base_models : list of tuples
Base model (name, estimator) pairs.
stacking_clf : StackingClassifier
Trained stacking ensemble.
X_train : pd.DataFrame
Training feature matrix.
y_train : pd.Series
Training labels.
n_splits : int
Number of CV folds (default: 5).
Returns
-------
cv_scores : dict
Dictionary mapping model name to array of fold ROC-AUC scores.
"""
tscv = TimeSeriesSplit(n_splits=n_splits)
cv_scores = {}
all_models = base_models + [("stacking_ensemble", stacking_clf)]
for name, model in all_models:
print(f"Cross-validating: {name} ...")
scores = cross_val_score(
model, X_train, y_train,
cv=tscv,
scoring="roc_auc",
n_jobs=-1
)
cv_scores[name] = scores
print(f" Mean AUC: {scores.mean():.4f} Std: {scores.std():.4f}")
return cv_scoresVisualization
The following visualization section renders four panels:
- Model Accuracy Comparison: Bar chart comparing test accuracy across all base models and the stacking ensemble.
- ROC-AUC Comparison: Bar chart comparing ROC-AUC scores; higher AUC indicates better probabilistic discrimination.
- ROC Curves: Receiver Operating Characteristic curves for all models plotted together; useful for visualizing the precision-recall tradeoff across thresholds.
- Signal Probability Distribution: Histogram of the stacking ensemble's predicted probabilities on the test set, with the long/short threshold boundaries marked. This distribution reveals the model's confidence profile and signal density.
def plot_ensemble_results(results, cv_scores, signal_df, y_test):
"""
Generate a 2x2 visualization panel summarizing model performance and signal output.
Parameters
----------
results : dict
Output from train_and_evaluate_models.
cv_scores : dict
Output from compute_cross_val_scores.
signal_df : pd.DataFrame
Output from generate_trading_signals.
y_test : pd.Series
True test labels.
"""
fig = plt.figure(figsize=(18, 13))
fig.suptitle(
"Notebook 297 — Ensemble Model Stacking: Performance Summary",
fontsize=15, fontweight="bold", y=1.01
)
gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.42, wspace=0.35)
model_names = list(results.keys())
accuracies = [results[m]["accuracy"] for m in model_names]
roc_aucs = [results[m]["roc_auc"] for m in model_names]
display_names = [m.replace("_", "\n") for m in model_names]
# Last bar (stacking) highlighted in red; base models in blue
bar_colors = ["#4C72B0"] * (len(model_names) - 1) + ["#C44E52"]
# --- Panel 1: Accuracy Comparison ---
ax1 = fig.add_subplot(gs[0, 0])
bars = ax1.bar(display_names, accuracies, color=bar_colors, edgecolor="white", linewidth=0.8)
ax1.set_title("Test Accuracy by Model", fontsize=12, fontweight="bold")
ax1.set_ylabel("Accuracy")
ax1.set_ylim(0.4, 0.75)
ax1.axhline(0.5, color="grey", linestyle="--", linewidth=0.8, label="Random baseline")
for bar, val in zip(bars, accuracies):
ax1.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.005,
f"{val:.3f}", ha="center", va="bottom", fontsize=9)
ax1.legend(fontsize=8)
ax1.tick_params(axis="x", labelsize=8)
# --- Panel 2: ROC-AUC Comparison ---
ax2 = fig.add_subplot(gs[0, 1])
bars2 = ax2.bar(display_names, roc_aucs, color=bar_colors, edgecolor="white", linewidth=0.8)
ax2.set_title("Test ROC-AUC by Model", fontsize=12, fontweight="bold")
ax2.set_ylabel("ROC-AUC")
ax2.set_ylim(0.4, 0.75)
ax2.axhline(0.5, color="grey", linestyle="--", linewidth=0.8, label="Random baseline")
for bar, val in zip(bars2, roc_aucs):
ax2.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.005,
f"{val:.3f}", ha="center", va="bottom", fontsize=9)
ax2.legend(fontsize=8)
ax2.tick_params(axis="x", labelsize=8)
# --- Panel 3: ROC Curves ---
ax3 = fig.add_subplot(gs[1, 0])
line_styles = ["-", "--", "-.", ":", "-"]
palette = ["#4C72B0", "#55A868", "#C44E52", "#8172B2", "#DD8452"]
for i, (name, data) in enumerate(results.items()):
fpr, tpr, _ = roc_curve(y_test, data["y_proba"])
ax3.plot(fpr, tpr,
label=f"{name.replace('_', ' ')} (AUC={data['roc_auc']:.3f})",
linestyle=line_styles[i % len(line_styles)],
color=palette[i % len(palette)],
linewidth=1.8)
ax3.plot([0, 1], [0, 1], "k--", linewidth=0.8, label="Random")
ax3.set_xlabel("False Positive Rate")
ax3.set_ylabel("True Positive Rate")
ax3.set_title("ROC Curves — All Models", fontsize=12, fontweight="bold")
ax3.legend(fontsize=7, loc="lower right")
# --- Panel 4: Signal Probability Distribution ---
ax4 = fig.add_subplot(gs[1, 1])
ax4.hist(signal_df["prob_up"], bins=40, color="#4C72B0", edgecolor="white",
linewidth=0.6, alpha=0.85)
ax4.axvline(0.55, color="#C44E52", linestyle="--", linewidth=1.5, label="Long threshold (0.55)")
ax4.axvline(0.45, color="#55A868", linestyle="--", linewidth=1.5, label="Short threshold (0.45)")
ax4.axvline(0.50, color="grey", linestyle=":", linewidth=1.0, label="Neutral boundary")
ax4.set_xlabel("Predicted Probability (P[Up])")
ax4.set_ylabel("Frequency")
ax4.set_title("Stacking Ensemble: Signal Probability Distribution", fontsize=12, fontweight="bold")
ax4.legend(fontsize=8)
plt.tight_layout()
plt.savefig("297_ensemble_stacking_results.png", dpi=150, bbox_inches="tight")
plt.show()
print("Visualization saved: 297_ensemble_stacking_results.png")Execution Pipeline
The complete pipeline is executed in sequential order. Each stage is explicitly called to maintain transparency and allow intermediate inspection at any step.
# ============================================================
# Stage 1: Generate synthetic financial dataset
# ============================================================
X, y, feature_names = generate_synthetic_market_data(n_samples=2000, noise_level=0.3)
# ============================================================
# Stage 2: Chronological train-test split
# ============================================================
X_train, X_test, y_train, y_test, split_idx = split_time_series_data(X, y, train_ratio=0.75)
# ============================================================
# Stage 3: Define heterogeneous base models
# ============================================================
base_models = define_base_models()
# Fix: Explicitly set n_jobs=1 for RandomForestClassifier to avoid partition error with TimeSeriesSplit
for i, (name, pipeline) in enumerate(base_models):
if name == "random_forest":
pipeline.named_steps['model'].n_jobs = 1
print(f"Modified n_jobs for RandomForestClassifier to 1 dynamically.")
break
# ============================================================
# Stage 4: Construct stacking ensemble
# ============================================================
stacking_clf = build_stacking_ensemble(base_models, n_splits=5)
# ============================================================
# Stage 5: Train all models and evaluate on test set
# ============================================================
results = train_and_evaluate_models(base_models, stacking_clf, X_train, X_test, y_train, y_test)
# ============================================================
# Stage 6: Print full classification reports
# ============================================================
print_classification_reports(results, y_test)
# ============================================================
# Stage 7: Cross-validation stability analysis
# ============================================================
cv_scores = compute_cross_val_scores(base_models, stacking_clf, X_train, y_train, n_splits=5)
# ============================================================
# Stage 8: Generate filtered trading signals
# ============================================================
signal_df = generate_trading_signals(stacking_clf, X_test, threshold=0.55)
# ============================================================
# Stage 9: Render performance visualizations
# ============================================================
plot_ensemble_results(results, cv_scores, signal_df, y_test)
print("\nPipeline execution complete.")Dataset shape: (1981, 11)
Class distribution: {1: 1024, 0: 957}
Training samples : 1485
Test samples : 496
Split index : 1485
Base models defined: ['random_forest', 'gradient_boosting', 'svm', 'logistic_regression_base']
Modified n_jobs for RandomForestClassifier to 1 dynamically.
Stacking ensemble constructed.
Meta-learner: LogisticRegression
CV strategy : KFold(n_splits=5, shuffle=False)
Training base model: random_forest ...
Accuracy : 0.4940
ROC-AUC : 0.4970
Training base model: gradient_boosting ...
Accuracy : 0.4940
ROC-AUC : 0.4824
Training base model: svm ...
Accuracy : 0.4980
ROC-AUC : 0.5027
Training base model: logistic_regression_base ...
Accuracy : 0.5181
ROC-AUC : 0.5189
Training stacking ensemble ...
Stacking Ensemble:
Accuracy : 0.4698
ROC-AUC : 0.4882
=======================================================
Classification Report --- random_forest
=======================================================
precision recall f1-score support
Down/Flat (0) 0.52 0.38 0.44 258
Up (1) 0.48 0.61 0.54 238
accuracy 0.49 496
macro avg 0.50 0.50 0.49 496
weighted avg 0.50 0.49 0.49 496
=======================================================
Classification Report --- gradient_boosting
=======================================================
precision recall f1-score support
Down/Flat (0) 0.52 0.45 0.48 258
Up (1) 0.48 0.55 0.51 238
accuracy 0.49 496
macro avg 0.50 0.50 0.49 496
weighted avg 0.50 0.49 0.49 496
=======================================================
Classification Report --- svm
=======================================================
precision recall f1-score support
Down/Flat (0) 0.52 0.37 0.43 258
Up (1) 0.48 0.64 0.55 238
accuracy 0.50 496
macro avg 0.50 0.50 0.49 496
weighted avg 0.50 0.50 0.49 496
=======================================================
Classification Report --- logistic_regression_base
=======================================================
precision recall f1-score support
Down/Flat (0) 0.56 0.37 0.44 258
Up (1) 0.50 0.68 0.58 238
accuracy 0.52 496
macro avg 0.53 0.52 0.51 496
weighted avg 0.53 0.52 0.51 496
=======================================================
Classification Report --- stacking_ensemble
=======================================================
precision recall f1-score support
Down/Flat (0) 0.48 0.20 0.28 258
Up (1) 0.47 0.76 0.58 238
accuracy 0.47 496
macro avg 0.47 0.48 0.43 496
weighted avg 0.47 0.47 0.42 496
Cross-validating: random_forest ...
Mean AUC: 0.5404 Std: 0.0416
Cross-validating: gradient_boosting ...
Mean AUC: 0.5132 Std: 0.0647
Cross-validating: svm ...
Mean AUC: 0.5268 Std: 0.0336
Cross-validating: logistic_regression_base ...
Mean AUC: 0.5343 Std: 0.0370
Cross-validating: stacking_ensemble ...
Mean AUC: 0.5372 Std: 0.0292
Signal threshold : 0.55
Long signals (1) : 102
Short signals (-1) : 2
No signal (0) : 392
Total bars : 496
Visualization saved: 297_ensemble_stacking_results.png Pipeline execution complete.