Does the signal predict anything?
Before building a strategy on a signal, ask whether it points the right way. Every day, a signal gives each stock a score, and a day later each stock has a return. The information coefficient (IC) is the correlation across stocks between the score and the later return. It ranges from -1 to 1; zero means no predictive power. Real signals are weak: an IC of 0.02 to 0.05 is already useful, because it is repeated over many stocks and many days.
import numpy as np
signal = np.array([0.3, -1.2, 0.8, 0.1, 1.5]) # today's score for five stocks
returns = np.array([0.01, -0.02, 0.005, -0.004, 0.03]) # their returns over the next day
s = signal - signal.mean()
r = returns - returns.mean()
ic = np.sum(s * r) / np.sqrt(np.sum(s ** 2) * np.sum(r ** 2)) # the Pearson correlation
print(round(float(ic), 4))
print(round(float(np.corrcoef(signal, returns)[0, 1]), 4)) # the same numberRank IC: robust to outliers
A Pearson correlation is dominated by extreme values: one stock with a huge score or a one-day jump of 40% can swing it. The rank IC replaces each array by the rank of its values (1 for the smallest) and correlates the ranks. It only asks whether stocks the signal puts higher end up higher, whatever the size of the numbers, so it is unchanged by any increasing transformation of the signal. It is the version most quant teams quote, and it is the Spearman correlation.
# The rank of each value: argsort gives the order, assigning 1..n through it gives the ranks
import numpy as np
values = np.array([30.0, 10.0, 20.0, 5.0])
order = np.argsort(values) # positions from smallest to largest: [3 1 2 0]
ranks = np.empty(len(values))
ranks[order] = np.arange(1, len(values) + 1)
print(order, ranks) # [3 1 2 0] [4. 2. 3. 1.]# Ranks via argsort, and a rank IC that shrugs off an extreme value
import numpy as np
def ranks(values):
order = np.argsort(values)
out = np.empty(len(values))
out[order] = np.arange(1, len(values) + 1)
return out
def ic(a, b):
a, b = a - a.mean(), b - b.mean()
return float(np.sum(a * b) / np.sqrt(np.sum(a ** 2) * np.sum(b ** 2)))
signal = np.array([0.3, -1.2, 0.8, 0.1, 1.5])
returns = np.array([0.01, -0.02, 0.005, -0.004, 0.03])
print(ranks(signal)) # [3. 1. 4. 2. 5.]
print(round(ic(signal, returns), 4), round(ic(ranks(signal), ranks(returns)), 4))
signal_with_outlier = signal.copy()
signal_with_outlier[4] = 1000.0 # one absurd score
print(round(ic(signal_with_outlier, returns), 4), round(ic(ranks(signal_with_outlier), ranks(returns)), 4))Consistency over time: the IC information ratio
One day's IC is noisy. Compute it every day to get a series, and look at both its mean and its steadiness. The ICIR, the mean IC divided by its standard deviation, rewards signals that are right a little every day over those that are right a lot on a few days. It is a Sharpe-like ratio for the signal itself. A daily series of IC values with mean 0.03 and standard deviation 0.06 has an ICIR of 0.5.
# A daily rank-IC series for a weak signal, and its ICIR
import numpy as np
def ranks(values):
out = np.empty(len(values))
out[np.argsort(values)] = np.arange(1, len(values) + 1)
return out
def ic(a, b):
a, b = a - a.mean(), b - b.mean()
return float(np.sum(a * b) / np.sqrt(np.sum(a ** 2) * np.sum(b ** 2)))
rng = np.random.default_rng(8)
daily_ics = []
for day in range(250):
signal = rng.normal(size=300) # 300 stocks
returns = 0.01 * (0.05 * signal + rng.normal(size=300)) # the signal explains a sliver of the return
daily_ics.append(ic(ranks(signal), ranks(returns)))
daily_ics = np.array(daily_ics)
print(round(float(daily_ics.mean()), 4), round(float(daily_ics.std(ddof=1)), 4))
print(round(float(daily_ics.mean() / daily_ics.std(ddof=1)), 3))- The IC is the correlation between today's signal across stocks and their later returns; 0.02 to 0.05 is useful, above 0.1 is suspicious.
- Rank IC correlates ranks (
argsorttwice): robust to outliers and unchanged by any increasing transform of the signal. - ICIR = mean IC / standard deviation of IC over many days: rewards steady skill; always evaluate out of sample.
Next up: historical VaR, the loss a portfolio should not exceed on all but the worst days.
Lab · what's graded
- Put the values in order with
ranks25 pts - Correlate signal and outcome with
ic25 pts - Ignore outliers with
rank_ic25 pts - Judge the consistency with
icir25 pts