Factor Evaluation

SkillDev tools

Evaluate alpha factor quality with IC analysis, quantile spreads, turnover, and decay. Use when deciding whether a signal has enough predictive power to trade.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Factor Evaluation skill

What this skill tells your AI

The instructions your AI receives, as published by ml4t/skills in validation/evaluate-factor/SKILL.md and read by ahel’s review.

A factor that looks predictive may be untradeable due to high turnover, rapid decay, or non-monotonic quantile spreads. Comprehensive evaluation before portfolio integration prevents costly live failures.

The Problem

Reporting a single backtest Sharpe ratio conflates signal quality with portfolio construction. A factor with IC 0.03 and low turnover can be more valuable than one with IC 0.05 and 80% daily turnover - whether IC is sufficient depends on breadth, turnover costs, and regime stability. Without decomposing signal quality into IC, quantile monotonicity, turnover, and decay, you cannot diagnose why a strategy fails or how to improve it.

The Pattern

WRONG

# Evaluate only via backtest Sharpe - hides factor-level issues
returns = run_backtest(signal)
sharpe = returns.mean() / returns.std() * np.sqrt(252)
print(f"Sharpe: {sharpe:.2f}")  # No idea why it works or doesn't

CORRECT

import numpy as np
from scipy import stats

# 1. Information Coefficient: rank correlation with forward returns
def compute_ic_series(signal, forward_returns, timestamps):
    """Per-period rank IC between signal and forward returns."""
    ic_by_period = []
    for t in np.unique(timestamps):
        mask = timestamps == t
        if mask.sum() < 10:
            continue
        ic, _ = stats.spearmanr(signal[mask], forward_returns[mask])
        ic_by_period.append(ic)
    return np.array(ic_by_period)

ic_series = compute_ic_series(signal, fwd_ret, dates)
print(f"IC Mean: {np.mean(ic_series):.4f}")
print(f"IC Std:  {np.std(ic_series):.4f}")
print(f"IC IR:   {np.mean(ic_series) / np.std(ic_series):.3f}")
print(f"IC t-stat: {np.mean(ic_series) / (np.std(ic_series) / np.sqrt(len(ic_series))):.2f}")

# 2. Quantile spreads: returns by signal quintile
n_quantiles = 5
quantile_labels = np.ceil(stats.rankdata(signal) / len(signal) * n_quantiles)
for q in range(1, n_quantiles + 1):
    q_ret = fwd_ret[quantile_labels == q].mean()
    print(f"  Q{q}: {q_ret:.4f}")
# Good factor: monotonic Q1 < Q2 < ... < Q5 (or reverse)

# 3. Turnover: how often positions change
quantiles_today = np.ceil(stats.rankdata(signal_today) / len(signal_today) * 5)
quantiles_yesterday = np.ceil(stats.rankdata(signal_yesterday) / len(signal_yesterday) * 5)
turnover = (quantiles_today != quantiles_yesterday).mean()
print(f"Turnover: {turnover:.1%}")

IC Decay Analysis

Signal power fades over time. Measure IC at multiple horizons to find optimal rebalance frequency:

for horizon in [1, 5, 10, 21, 63]:
    fwd = returns.shift(-horizon)
    ic = stats.spearmanr(signal[~np.isnan(fwd)], fwd[~np.isnan(fwd)])[0]
    print(f"  {horizon:>3}d IC: {ic:.4f}")
# IC peaks at the natural signal horizon; decays beyond it

Guardrails

  • IC alone is insufficient - a factor with high IC but non-monotonic quintiles is unreliable
  • High turnover (>30% daily) signals that transaction costs may consume the alpha
  • Always compute t-statistics with HAC (Newey-West) standard errors for autocorrelated IC series
  • Decay analysis determines rebalance frequency - rebalancing faster than the peak-IC horizon wastes costs
  • IC is a learnability screen, not a strategy - a positive IC only means the signal has information; tradability requires checking turnover, costs, and capacity

Production Implementation

ml4t-diagnostic provides validated IC computation with HAC corrections:

from ml4t.diagnostic.api import compute_ic_hac_stats, cross_sectional_ic_series

ic_series = cross_sectional_ic_series(
    signal_frame,
    return_frame,
    pred_col="signal",
    ret_col="forward_return",
    date_col="date",
    entity_col="symbol",
)
ic_stats = compute_ic_hac_stats(ic_series)  # Newey-West corrected
print(f"IC: {ic_stats['mean_ic']:.4f} (t={ic_stats['t_stat']:.2f})")

Checklist

  • IC mean, IC IR, and worst-fold IC computed (IC IR > 0.5; worst-fold IC same sign as mean)
  • Quantile returns checked for monotonicity
  • Turnover estimated and compared against cost model
  • Decay analysis performed to determine rebalance frequency
  • t-statistics use HAC standard errors, not naive SE

Signals

GitHub stars
20
Forks
11
Last commit
Sep 2026
Advanced
Item type
skill
Key
ml4t-evaluate-factor
Source
github.com/ml4t/skills