Back To Top

March 19, 2025

How to Evaluate Factors to Generate Alpha

Not All Factors Are Created Equal - How to Find, Validate, and Test the Best Alpha Factors Using Alphalens in Python

Finding consistent alpha in the market is hard. Traders could rely on intuition, but this might not offer a systematic and clear edge.

The key is identifying repeatable and testable patterns — factors — that are correlated with future returns.

In this article, we will walk through a quantitative framework for evaluating the performance of alpha factors.

Using a simple mean-reversion strategy, we’ll compute Z-score factors across multiple stocks to identify key price deviations. 

Factors will be analyzed in a cross-sectional manner using Alphalens. Stocks are then ranked into quantiles based on the strength of the factors

Notebook Demo Entreprenerdly - Evaluating Factors to Generate Alpha AVIF

This article is structured as follows

  • Constructing Simple Mean-Reversion Z-Score Factor
  • Preparing a Factor Dataset for Alphalens
  • Evaluating Factor Performance with Alphalens
  • Key Limitations and Concluding Thoughts

End-to-end notebook is provided below so that you can adjust the parameters and strategy as needed.

1. Factor Investing and Alpha Generation

What Are Factors?

Factor investing is a systematic approach to selecting stocks based on quantifiable characteristics that explain differences in returns. 

This concept dates back to Fama and French (1992), who introduced the Three-Factor Model, which expanded on the traditional Capital Asset Pricing Model by adding size and value factors to explain stock returns better.

Over time, researchers and practitioners have identified several other factors, including:

  • Value: Stocks that appear cheap based on fundamentals (e.g., low P/E or P/B ratios) tend to outperform expensive stocks. (Fama & French, 1992)
  • Momentum: Stocks that have performed well recently tend to continue performing well in the short term. (Jegadeesh & Titman, 1993)
  • Quality: Stocks with strong profitability, low debt, and stable earnings deliver higher risk-adjusted returns. (Novy-Marx, 2013)
  • Volatility & Low Beta: Lower-volatility stocks tend to produce better risk-adjusted returns than high-volatility stocks. (Baker, Bradley, & Wurgler, 2011)
  • Formulaic Alphas: hand-crafted, rule-based trading signals derived from mathematical expressions applied to high-frequency price and volume data. (Zura Kakushadze, 2016) 

Cross-Sectional Factor Analysis

Instead of analyzing a single stock, we evaluate a large set of stocks at each point in time. This is known as cross-sectional analysis, and it is widely used in quantitative finance to rank stocks based on factor strength.

For instance, we can rank all S&P 500 stocks by their Z-score and divide them into five quantiles:

  • Top 20% (Q5): Stocks with the highest Z-scores (potential short candidates).
  • Bottom 20% (Q1): Stocks with the lowest Z-scores (potential long candidates).

By tracking the performance of these quantiles over time, we can assess whether the factor is predictive. If Q1 consistently outperforms Q5, then the Z-score factor has value.

2. Constructing a Mean-Reversion Z-Score Factor

We implement a simple mean-reversion trading strategy to illustrate the construction of predictive factors.

Mean reversion is a well-studied short-term factor, rooted in behavioral finance — traders overreact to news, pushing prices away from their fair value, which leads to corrections.

To quantify these deviations, we compute a Z-score to measure the deviation of the current price from a long-term average. 

EWMA-Based Z-Score Formula

For the long-term average estimation we use ‘Exponentially Weighted Moving Average’.

The Z-score in our implementation is calculated as:

where:

  • Rt​ = log return at time t
  • EWMAμ​ = exponentially weighted moving average of returns
  • EWMAσ​ = exponentially weighted standard deviation of returns

To determine the optimal lookback period, we use a half-life parameter, which controls how quickly past data loses relevance.

A smaller half-life reacts faster to price changes, while a larger half-life smooths fluctuations more gradually.

Finally, we apply winsorization to remove extreme outliers, ensuring that rare price spikes or crashes do not distort the factor signal / quantiles.

				
					import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import warnings
from scipy.stats.mstats import winsorize

warnings.filterwarnings("ignore")  # Suppress warnings for cleaner output
plt.style.use("dark_background")
				
			
				
					def compute_zscore(returns, halflife):
    """
    Computes a winsorized Z-score of returns using an EWMA:
      - Halflife controls how much weight recent data has
      - Winsorize the raw Z-score at the 1st and 99th percentiles
    """
    mean_ewma = returns.ewm(halflife=halflife, adjust=False).mean()
    std_ewma = returns.ewm(halflife=halflife, adjust=False).std()

    # Calculate the raw Z-score
    raw_zscore = (returns - mean_ewma) / std_ewma

    # Winsorize to remove extreme outliers (e.g., beyond ~1st and 99th percentiles)
    # Convert the masked array back to a Series to preserve the index
    stable_zscore = pd.Series(winsorize(raw_zscore, limits=[0.01, 0.01]), index=raw_zscore.index)

    return stable_zscore
				
			

Trading Strategy Using EWMA Z-Scores

The goal is to detect short-term price deviations and trade against them.

Trading Signals:

  • Buy when Z-score < -2 (expecting a price increase).
  • Sell when Z-score > 2 (expecting a price decrease).
				
					def strategy_performance(prices, zscore, threshold):
    """
    Backtests a simple mean reversion strategy:
      - Long when Z-score < -threshold
      - Short when Z-score > threshold
      - No position otherwise
    Returns the cumulative strategy return over the test period.
    """
    # Shift signals by 1 to trade on the next day's open
    long_positions = (zscore < -threshold).shift(1)
    short_positions = (zscore > threshold).shift(1)

    # Daily log returns
    daily_returns = np.log(prices / prices.shift(1))

    # Strategy return: long => +daily_returns, short => -daily_returns
    strat_ret = daily_returns.where(long_positions, 0) - daily_returns.where(short_positions, 0)

    # Convert cumulative log returns to approximate total return
    total_return = np.exp(strat_ret.fillna(0).sum()) - 1.0
    return total_return
				
			

Optimization:

  • Tune the half-life parameter in the EWMA to find the best signal strength.
  • Backtest across multiple stocks to evaluate performance.

While we used an optimized EWMA-based mean-reversion strategy, users can implement a simpler version using a fixed rolling moving average.

				
					# --- Parameters --- #
tickers = ["AAPL", "MSFT", "TSLA", "AMZN", "GOOGL", "ASML"]
start_date = "2010-01-01"
split_date = "2020-01-01"  # Marks the transition from training to testing period
end_date = "2025-12-31"
THRESHOLD = 2  # Z-score threshold for generating trade signals
half_life_candidates = [126, 252]  # Potential long-term mean reversion factors

# --- Download historical price data --- #
df = yf.download(tickers, start=start_date, end=end_date, progress=False)['Close']

# --- Split data into train and test sets --- #
train = df[:split_date].copy()  # Training data
test = df[split_date:].copy()   # Test data

DAYS = 10  # Lookback period for log returns

# --- Compute log returns over the chosen lookback period --- #
train_returns = np.log(train / train.shift(DAYS))
test_returns = np.log(test / test.shift(DAYS))

# --- Find the best half-life on the training set for each ticker --- #
best_half_life = {}
for ticker in tickers:
    best_hl = None
    best_perf = -9999  # Start with a very low benchmark performance

    for hl in half_life_candidates:
        zscore_train = compute_zscore(train_returns[ticker], hl)
        perf = strategy_performance(train[ticker], zscore_train, THRESHOLD)

        if perf > best_perf:
            best_perf = perf
            best_hl = hl

    best_half_life[ticker] = best_hl
				
			

Show Buy and Sell Signals on the Test Set

Now that we have optimized the EWMA Z-score factor and selected the best half-life for each stock, the next step is to apply it to the test dataset. 

This will allow us to evaluate how well the factor performs in an out-of-sample environment.

				
					# --- Evaluate the strategy on the test set --- #
for ticker in tickers:
    final_hl = best_half_life[ticker]
    zscore_test = compute_zscore(test_returns[ticker], final_hl)

    # Identify buy/sell signals
    buy_signals = zscore_test < -THRESHOLD
    sell_signals = zscore_test > THRESHOLD

    # Compute performance
    test_perf = strategy_performance(test[ticker], zscore_test, THRESHOLD)
    print(f"{ticker} - Best Half-Life: {final_hl}, Test Return: {test_perf:.2%}")

    # --- Plot results --- #
    fig, axes = plt.subplots(nrows=2, figsize=(14, 8), sharex=True)

    # Price plot
    axes[0].plot(test[ticker], label=f"{ticker} Price", color='white')
    axes[0].scatter(test.index[buy_signals], test[ticker][buy_signals],
                    label="Buy Signal", marker="^", color='green', alpha=1, s=100)
    axes[0].scatter(test.index[sell_signals], test[ticker][sell_signals],
                    label="Sell Signal", marker="v", color='red', alpha=1, s=100)
    axes[0].set_title(f"{ticker} Price (Test Period) - HL={final_hl}")
    axes[0].legend()

    # Z-score plot
    axes[1].plot(zscore_test, color='blue', label="Z-Score")
    axes[1].axhline(-THRESHOLD, linestyle="--", color="green", alpha=0.7, label=f"Buy Threshold (-{THRESHOLD})")
    axes[1].axhline(THRESHOLD, linestyle="--", color="red", alpha=0.7, label=f"Sell Threshold ({THRESHOLD})")
    axes[1].set_title(f"{ticker} Z-Score (Test Period) - HL={final_hl}")
    axes[1].legend()

    plt.tight_layout()
    plt.show()
				
			
figure 1. mean-reversion strategy AMZN (OPTIMIZED)
figure 1. mean-reversion strategy Apple (OPTIMIZED)
figure 1. mean-reversion strategy ASML (OPTIMIZED)
figure 1. mean-reversion strategy TSLA (OPTIMIZED)

Figure 1: EWMA-based mean-reversion strategy - buy and sell signals overlaid on stock prices (top) and corresponding Z-scores with thresholds (bottom) for multiple stocks during the test period.

3. Creating a Z-Score Dataset for Factor Analysis

Factor analysis requires structured, cross-sectional data to evaluate how signals perform across different stocks. 

To achieve this, we transform our Z-score calculations into a dataset suitable for factor research.

This dataset serves two key purposes:

  1. Standardized Factor Storage → Ensures each stock’s Z-score is stored alongside its price and timestamp.
  2. Preparation for Alphalens Analysis → Allows for ranking stocks into quantiles and computing forward returns for predictive evaluation.

Z-Score Dataset with Prices

We format the dataset with the following columns:

  • Date: Timestamp for each observation.
  • Ticker: Stock symbol.
  • Price: The stock’s closing price.
  • Z-Score: Computed EWMA-based Z-score for mean-reversion analysis.
Prev Post

Avoid This Critical Mistake With Bitcoin That Could Cost You

Next Post

Has the Cryptocurrency Market Reached the End of the Trump…

post-bars
Mail Icon

Newsletter

Get Every Weekly Update & Insights

[mc4wp_form id=]

Leave a Comment