Factor Research Workflow

SkillDev tools

Systematic factor research from hypothesis through IC analysis, decay profiling, and capacity assessment. Use when developing a new alpha factor end-to-end.

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 Research Workflow skill

What this skill tells your AI

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

Testing one factor on one period and deploying is data mining. Systematic factor research requires IC significance, stability across subperiods, decay profiling, and capacity estimation before any factor enters a model.

The Problem

A researcher computes 12-month momentum, sees rank IC of 0.04, and adds it to the model. Six months later the factor collapses because the original test never checked significance, stability, decay, or capacity.

The Pattern

WRONG

# Test one factor, one period, deploy on a single positive number
from scipy.stats import spearmanr

factor = prices.pct_change(252)  # 12-month momentum
fwd_ret = prices.pct_change(21).shift(-21)  # 1-month forward return

ic, _ = spearmanr(factor.dropna(), fwd_ret.dropna())
print(f"IC: {ic:.3f}")  # 0.04 - looks good, ship it

CORRECT

import numpy as np
import polars as pl
from scipy.stats import spearmanr

ic_series = []
for date in rebalance_dates:
    cross_section = data.filter(pl.col("timestamp") == date)
    ic, _ = spearmanr(cross_section["factor"], cross_section["fwd_ret"])
    ic_series.append({"timestamp": date, "ic": ic})

ic_df = pl.DataFrame(ic_series)
ic_mean = ic_df["ic"].mean()
ic_std = ic_df["ic"].std()
n = len(ic_df)
t_stat = ic_mean / (ic_std / np.sqrt(n))  # Simplified; use HAC for production
midpoint = n // 2
ic_first_half = ic_df[:midpoint]["ic"].mean()
ic_second_half = ic_df[midpoint:]["ic"].mean()
for horizon in [1, 5, 10, 21, 63]:
    # Cross-sectional, as above. Dropping nulls from the factor and the forward
    # return separately leaves two series of different length and dates.
    col, ics = f"fwd_ret_{horizon}", []
    for day in data.drop_nulls(["factor", col]).partition_by("timestamp"):
        ics.append(spearmanr(day["factor"], day[col])[0])
    print(f"  {horizon}d IC: {np.nanmean(ics):.4f}")
print(f"IC: {ic_mean:.4f} (t={t_stat:.2f})")
print(f"Stability: {ic_first_half:.4f} / {ic_second_half:.4f}")
assert abs(t_stat) > 2.0, "IC not statistically significant"
assert ic_first_half * ic_second_half > 0, "IC sign flipped across subperiods"

Five-Gate Evaluation

GateMetricThresholdSkill Reference
SignificanceIC t-stat (HAC)> 2.0ml4t-information-coefficient
StabilitySubperiod IC sign agreementSame sign in all halvesml4t-feature-validation
DecayHalf-life vs rebalance frequencyHalf-life > 2x rebalance periodml4t-horizon-design
UniquenessCorrelation with existing factors< 0.7 rank correlationml4t-feature-families
CapacityTurnover-implied trading volumeTradeable at target AUMml4t-evaluate-factor

Guardrails

  • If IC > 0.10 on daily equity data, suspect lookahead bias - cross-sectional equity ICs are typically 0.02-0.05 (Grinold & Kahn, Kakushadze)
  • If factor turnover exceeds 50% monthly, capacity is likely constrained - check with ml4t-evaluate-factor
  • If IC is high but quantile returns are non-monotonic, the signal is noisy and may not translate to returns
  • If subperiod ICs disagree in sign, the factor is likely spurious regardless of full-period IC

Production Implementation

from ml4t.diagnostic.api import compute_ic_hac_stats, cross_sectional_ic_series
from ml4t.diagnostic.metrics import analyze_feature_outcome

ic = cross_sectional_ic_series(
    factor_frame,
    return_frame,
    pred_col="factor",
    ret_col="forward_return",
    date_col="date",
    entity_col="symbol",
)
stats = compute_ic_hac_stats(ic)  # Newey-West adjusted t-stat

report = analyze_feature_outcome(
    predictions=factor_frame,
    prices=price_frame,
    pred_col="factor",
    price_col="close",
    date_col="date",
    group_col="symbol",
    horizons=[1, 5, 21],
)

Checklist

  • Economic hypothesis documented with mechanism and expected IC range
  • Factor computed with no lookahead (lagged by at least one period)
  • IC series computed cross-sectionally for every rebalance date
  • IC significance tested with HAC standard errors (t > 2.0)
  • Subperiod stability verified (IC same sign in both halves)
  • Decay, uniqueness, and capacity checked before deployment

Signals

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