Source code for finance_calcs.alpha
"""Alpha / signal evaluation as polars expressions.
These functions are designed to be composed inside ``group_by("date").agg(...)``
to produce a cross-sectional information-coefficient time series, then
aggregated across time with :func:`information_coefficient_ratio` and friends.
"""
from __future__ import annotations
from collections.abc import Mapping
import polars as pl
from ._periods import PeriodLike, _bucket_or_none, _check_window_period
__all__ = [
"forward_returns",
"hit_rate",
"information_coefficient",
"information_coefficient_by_horizon",
"information_coefficient_conditional",
"information_coefficient_decay",
"information_coefficient_pearson",
"information_coefficient_ratio",
"information_coefficient_spearman",
"information_coefficient_statistics",
]
__finance_namespace__ = [
"forward_returns",
"hit_rate",
"information_coefficient",
"information_coefficient_by_horizon",
"information_coefficient_conditional",
"information_coefficient_decay",
"information_coefficient_pearson",
"information_coefficient_ratio",
"information_coefficient_spearman",
]
[docs]
def forward_returns(price: pl.Expr, horizon: int = 1) -> pl.Expr:
"""Forward simple return over ``horizon`` observations.
Args:
price: Price series.
horizon: Look-ahead horizon in observations.
Returns:
Expression yielding ``price.shift(-horizon) / price - 1``.
"""
return price.shift(-horizon) / price - 1.0
[docs]
def information_coefficient_pearson(signal: pl.Expr, forward_returns: pl.Expr) -> pl.Expr:
"""Pearson information coefficient.
Args:
signal: Signal / alpha series.
forward_returns: Forward-return series of the same length.
Returns:
Scalar correlation expression.
"""
return pl.corr(signal, forward_returns, method="pearson")
[docs]
def information_coefficient_spearman(signal: pl.Expr, forward_returns: pl.Expr) -> pl.Expr:
"""Spearman rank information coefficient.
Args:
signal: Signal / alpha series.
forward_returns: Forward-return series of the same length.
Returns:
Scalar rank-correlation expression.
"""
return pl.corr(signal, forward_returns, method="spearman")
[docs]
def information_coefficient(signal: pl.Expr, forward_returns: pl.Expr, *, method: str = "spearman") -> pl.Expr:
"""Information coefficient using the requested correlation method."""
return pl.corr(signal, forward_returns, method=method)
[docs]
def information_coefficient_conditional(
signal: pl.Expr,
forward_returns: pl.Expr,
condition: pl.Expr,
*,
method: str = "spearman",
) -> pl.Expr:
"""Information coefficient on observations matching ``condition``."""
return pl.corr(signal.filter(condition), forward_returns.filter(condition), method=method)
[docs]
def information_coefficient_by_horizon(
signal: pl.Expr,
forward_returns: pl.Expr,
*,
method: str = "spearman",
) -> pl.Expr:
"""Information coefficient for one forward-return horizon."""
return pl.corr(signal, forward_returns, method=method)
[docs]
def information_coefficient_decay(
signal: pl.Expr,
forward_returns_by_horizon: Mapping[int, pl.Expr],
*,
method: str = "spearman",
prefix: str = "information_coefficient_",
) -> list[pl.Expr]:
"""Build one horizon IC expression per forward-return horizon."""
return [
information_coefficient_by_horizon(signal, forward_return, method=method).alias(f"{prefix}{horizon}")
for horizon, forward_return in sorted(forward_returns_by_horizon.items())
]
[docs]
def information_coefficient_ratio(
information_coefficient: pl.Expr,
*,
window: int | None = None,
period: PeriodLike | None = None,
date: pl.Expr | None = None,
) -> pl.Expr:
"""IC information ratio — ``mean(ic) / std(ic)``.
``window=None`` → scalar; ``window=N`` → rolling IR over each
trailing ``N``-observation window; ``period=...`` → per-bucket IR.
"""
_check_window_period(window, period)
bucket = _bucket_or_none(date, period)
if bucket is not None:
return information_coefficient.mean().over(bucket) / information_coefficient.std().over(bucket)
if window is None:
return information_coefficient.mean() / information_coefficient.std()
return information_coefficient.rolling_mean(window) / information_coefficient.rolling_std(window)
[docs]
def hit_rate(signal: pl.Expr, forward_returns: pl.Expr) -> pl.Expr:
"""Fraction of observations where signal and forward-return signs agree.
Args:
signal: Signal series.
forward_returns: Forward return series.
Returns:
Scalar mean expression in ``[0, 1]``.
"""
same = (signal.sign() == forward_returns.sign()).cast(pl.Float64)
return same.mean()
[docs]
def information_coefficient_statistics(information_coefficient: pl.Series) -> dict[str, float | int]:
"""Summary statistics of an IC time series.
Args:
information_coefficient: Information-coefficient time series.
Returns:
Dict with mean, standard deviation, information ratio, t-statistic,
positive fraction, and observation count.
"""
arr = information_coefficient.drop_nulls().drop_nans() if hasattr(information_coefficient, "drop_nans") else information_coefficient.drop_nulls()
n = arr.len()
if n == 0:
return {
"mean": float("nan"),
"standard_deviation": float("nan"),
"information_ratio": float("nan"),
"t_statistic": float("nan"),
"positive_fraction": float("nan"),
"observation_count": 0,
}
mean = float(arr.mean())
standard_deviation = float(arr.std()) if n > 1 else 0.0
information_ratio = mean / standard_deviation if standard_deviation > 0 else float("nan")
t_statistic = information_ratio * (n**0.5) if standard_deviation > 0 else float("nan")
positive_fraction = float((arr > 0).cast(pl.Float64).mean())
return {
"mean": mean,
"standard_deviation": standard_deviation,
"information_ratio": information_ratio,
"t_statistic": t_statistic,
"positive_fraction": positive_fraction,
"observation_count": int(n),
}