Notebooks/Classification Signal Model
Signals·ML Models·Intermediate

Classification Signal Model

Predict trade direction (long/short/flat) using a classification model trained on lagged returns, volume, and volatility features.

classificationdirectionML

Signals — Classification Models


1. Dependency Installation

[ ]
!pip install pandas numpy plotly scikit-learn
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1)
Requirement already satisfied: scikit-learn in /usr/local/lib/python3.12/dist-packages (1.6.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4)
Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1)
Requirement already satisfied: scipy>=1.6.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.16.3)
Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.5.3)
Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (3.6.0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)

2. Library Imports

[ ]
import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score

3. Strategy Overview

Classification models predict the discrete direction of future price movement (+1 up, 0 down) from engineered technical features.

Models evaluated:

  • Logistic Regression: Linear baseline; interpretable coefficients.
  • Random Forest: Ensemble of decision trees; handles non-linear feature interactions.
  • Gradient Boosting: Sequential boosting; typically higher accuracy at the cost of longer training.

Evaluation metrics:

  • Accuracy, Precision, Recall, F1 — standard classification metrics.
  • ROC-AUC — rank-ordering quality; preferable for imbalanced datasets.
  • Confusion matrix — breakdown of true/false positives and negatives.

Limitation: Classification accuracy alone does not guarantee profitability; a model may be directionally correct but generate signals at poor risk/reward points. Backtest P&L evaluation is required.

4. Data Generation

[ ]
def generate_data(periods: int) -> pd.DataFrame:
    """
    Generate synthetic OHLCV price data using a geometric random walk.

    Parameters
    ----------
    periods : int
        Number of 1-minute bars to generate.

    Returns
    -------
    pd.DataFrame
        DataFrame with columns: open, high, low, close, volume, datetime.
    """
    start_date     = pd.to_datetime("2024-01-01 00:00:00+00:00")
    datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
    price_data = []
    last_close = 42000
    for i in range(periods):
        open_price  = last_close + np.random.normal(0, last_close * 0.0005)
        close_price = open_price + np.random.normal(0, last_close * 0.005)
        body_high   = max(open_price, close_price)
        body_low    = min(open_price, close_price)
        high_price  = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        low_price   = min(body_low  - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        if high_price < low_price:
            high_price, low_price = low_price, high_price
        price_data.append({
            "open":  max(1, int(open_price)),
            "high":  max(1, int(high_price)),
            "low":   max(1, int(low_price)),
            "close": max(1, int(close_price)),
        })
        last_close = close_price
    df = pd.DataFrame(price_data, index=datetime_index)
    df.index.name = "datetime"
    df["volume"]   = np.random.uniform(100.0, 500.0, periods)
    df["datetime"] = df.index.to_series()
    return df.reset_index(drop=True)

df = generate_data(500)
display(df.head())
open high low close volume datetime
0 42009 42382 41992 42227 378.076885 2024-01-01 00:00:00+00:00
1 42273 42503 42266 42503 468.429426 2024-01-01 00:01:00+00:00
2 42497 42593 42385 42584 498.404160 2024-01-01 00:02:00+00:00
3 42593 42856 42513 42538 449.158083 2024-01-01 00:03:00+00:00
4 42548 42691 42308 42319 378.990673 2024-01-01 00:04:00+00:00

5. Feature Engineering (Shared Pipeline)

[ ]
def build_features(df: pd.DataFrame, horizon: int = 5) -> tuple:
    """
    Compute technical features and binary labels for classification.

    Parameters
    ----------
    df : pd.DataFrame
        OHLCV DataFrame.
    horizon : int
        Forward bars for label computation.

    Returns
    -------
    tuple
        (X_train, X_test, y_train, y_test, feature_names)
    """
    df = df.copy().sort_values("datetime", ignore_index=True)
    df["return_1"]  = df["close"].pct_change(1)
    df["return_5"]  = df["close"].pct_change(5)
    delta = df["close"].diff()
    gain  = delta.clip(lower=0).rolling(14).mean()
    loss  = (-delta.clip(upper=0)).rolling(14).mean()
    df["rsi_14"]   = 100 - 100 / (1 + gain / loss.replace(0, np.nan))
    df["sma_ratio"] = df["close"].rolling(10).mean() / df["close"].rolling(20).mean()
    tr = pd.concat([
        df["high"] - df["low"],
        (df["high"] - df["close"].shift(1)).abs(),
        (df["low"]  - df["close"].shift(1)).abs(),
    ], axis=1).max(axis=1)
    df["atr_pct"]  = tr.rolling(14).mean() / df["close"]
    df["vol_ratio"] = df["volume"] / df["volume"].rolling(20).mean()
    df["label"]    = (df["close"].shift(-horizon) > df["close"]).astype(int)

    features = ["return_1", "return_5", "rsi_14", "sma_ratio", "atr_pct", "vol_ratio"]
    df.dropna(subset=features + ["label"], inplace=True)

    X, y = df[features].values, df["label"].values
    split = int(len(X) * 0.8)
    X_tr, X_te = X[:split], X[split:]
    y_tr, y_te = y[:split], y[split:]

    sc = StandardScaler()
    X_tr = sc.fit_transform(X_tr)
    X_te = sc.transform(X_te)
    return X_tr, X_te, y_tr, y_te, features

X_train, X_test, y_train, y_test, feat_names = build_features(df, horizon=5)
print(f"Train: {len(X_train)} | Test: {len(X_test)} | Features: {feat_names}")
Train: 384 | Test: 97 | Features: ['return_1', 'return_5', 'rsi_14', 'sma_ratio', 'atr_pct', 'vol_ratio']

6. Model Training and Evaluation

[ ]
def classification_model(X_train, X_test, y_train, y_test) -> dict:
    """
    Train and evaluate multiple classification models.

    Core logic
    ----------
    1. Instantiate three classifiers (Logistic Regression, Random Forest,
       Gradient Boosting) with fixed random seeds for reproducibility.
    2. Fit each model on the training set.
    3. Predict class labels and probabilities on the test set.
    4. Compute accuracy, ROC-AUC, and a full classification report.

    Parameters
    ----------
    X_train, X_test : np.ndarray  Scaled feature matrices.
    y_train, y_test : np.ndarray  Binary labels.

    Returns
    -------
    dict
        Model name → {'model', 'predictions', 'probabilities', 'auc', 'report'}
    """
    models = {
        "Logistic Regression":   LogisticRegression(max_iter=1000, random_state=42),
        "Random Forest":         RandomForestClassifier(n_estimators=100, random_state=42),
        "Gradient Boosting":     GradientBoostingClassifier(n_estimators=100, random_state=42),
    }
    results = {}
    for name, model in models.items():
        model.fit(X_train, y_train)
        preds = model.predict(X_test)
        proba = model.predict_proba(X_test)[:, 1]
        auc   = roc_auc_score(y_test, proba)
        results[name] = {
            "model":         model,
            "predictions":   preds,
            "probabilities": proba,
            "auc":           auc,
            "report":        classification_report(y_test, preds, output_dict=True),
        }
        print(f"\n{'='*50}\n{name}\nROC-AUC: {auc:.4f}")
        print(classification_report(y_test, preds))
    return results

results = classification_model(X_train, X_test, y_train, y_test)

==================================================
Logistic Regression
ROC-AUC: 0.4370
              precision    recall  f1-score   support

           0       0.62      0.27      0.38        66
           1       0.29      0.65      0.40        31

    accuracy                           0.39        97
   macro avg       0.46      0.46      0.39        97
weighted avg       0.52      0.39      0.39        97


==================================================
Random Forest
ROC-AUC: 0.4707
              precision    recall  f1-score   support

           0       0.67      0.33      0.44        66
           1       0.31      0.65      0.42        31

    accuracy                           0.43        97
   macro avg       0.49      0.49      0.43        97
weighted avg       0.55      0.43      0.44        97


==================================================
Gradient Boosting
ROC-AUC: 0.5103
              precision    recall  f1-score   support

           0       0.67      0.21      0.32        66
           1       0.32      0.77      0.45        31

    accuracy                           0.39        97
   macro avg       0.49      0.49      0.39        97
weighted avg       0.55      0.39      0.36        97

8. Visualization — Confusion Matrices

To further evaluate the performance of each classification model, we will visualize their confusion matrices. A confusion matrix provides a detailed breakdown of correct and incorrect predictions for each class, showing true positives, true negatives, false positives, and false negatives.

[7]
import matplotlib.pyplot as plt
import seaborn as sns

for name, res in results.items():
    cm = confusion_matrix(y_test, res["predictions"])
    plt.figure(figsize=(6, 5))
    sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
                xticklabels=['Predict 0', 'Predict 1'],
                yticklabels=['Actual 0', 'Actual 1'])
    plt.title(f'Confusion Matrix - {name}')
    plt.xlabel('Predicted Label')
    plt.ylabel('True Label')
    plt.show()
cell output
cell output
cell output
[ ]