Compute Features

SkillDev tools

Systematic feature computation across multiple assets with group-aware operations. Use when computing technical or fundamental features for a panel of securities.

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 Compute Features skill

What this skill tells your AI

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

Computing features across a panel of assets requires group-aware operations - a global rolling mean mixes Apple's history with Tesla's. Every windowed statistic must be computed per symbol.

The Problem

Features computed with global statistics bleed information across assets. A z-score computed over the full dataframe uses one symbol's volatility to normalize another. Worse, using .mean() on the full column leaks future data from late-arriving symbols into early rows.

The Pattern

WRONG

import polars as pl

# Global statistics mix symbols and leak future
df = df.with_columns(
    mom_zscore=(pl.col("returns") - pl.col("returns").mean())
    / pl.col("returns").std()
)

CORRECT

import polars as pl

# Per-symbol rolling window - no cross-contamination, no lookahead
df = df.sort("symbol", "timestamp").with_columns(
    mom_zscore=(
        (pl.col("returns") - pl.col("returns").rolling_mean(504).shift(1))  # 504 ≈ 2 trading years
        / pl.col("returns").rolling_std(504).shift(1)
    ).over("symbol")
)

Windowed Aggregations

Three window types, each with different use cases:

# Rolling: fixed lookback, discards old data
pl.col("close").pct_change(21).over("symbol")              # 21-day momentum

# Long-horizon rolling: large window approximates expanding
pl.col("returns").rolling_mean(504).shift(1).over("symbol")   # 504 days ≈ 2 trading years

# Cross-sectional: rank across all symbols at each timestamp
pl.col("momentum").rank().over("timestamp")                 # Peer rank

Always .shift(1) expanding/rolling statistics to avoid using the current bar's value in its own feature.

Multi-Feature Computation

features = df.sort("symbol", "timestamp").with_columns(
    momentum_21d=pl.col("close").pct_change(21).over("symbol"),
    volatility_21d=pl.col("returns").rolling_std(21).over("symbol"),
    volume_ratio=pl.col("volume")
    / pl.col("volume").rolling_mean(21).shift(1).over("symbol"),
)

Guardrails

  • Every .over("symbol"): any rolling/expanding stat without .over("symbol") on panel data is a bug
  • Shift before use: expanding stats need .shift(1) to avoid including the current observation
  • Sort order matters: always sort("symbol", "timestamp") before windowed operations
  • Horizon alignment: feature lookback should match label horizon - a 63-day momentum feature on a 5-day label captures noise, not signal

Production Implementation

ml4t-engineer provides config-driven computation with dependency resolution for single-series or per-symbol feature pipelines. For cross-sectional panels, keep the explicit grouped Polars logic from this skill and apply registry features per symbol rather than assuming automatic panel partitioning.

from ml4t.engineer import compute_features, feature_catalog

# Discover available features
print(feature_catalog.list())

# Per-asset registry features
spy_features = compute_features(spy_prices, ["rsi", "macd", "bollinger_bands"])

# YAML configs are also supported
feature_specs = compute_features(spy_prices, "config/features.yaml")

Checklist

  • All windowed features use .over("symbol") for panel data
  • Expanding/rolling stats are .shift(1) before use
  • Data is sorted by ("symbol", "timestamp") before feature computation
  • No global .mean() / .std() on panel columns
  • Cross-sectional features use .over("timestamp")

Signals

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