QuantConnect
Fama French
Introduction
The Fama-French Factors dataset by QuantConnect gives you the academic factor returns that quants use to explain where a portfolio's performance actually came from: the market, size, value, profitability, investment and momentum premiums, plus the risk-free rate. It covers the whole US equity market, goes back to January 1998, and updates daily.
QuantConnect computes the factors instead of republishing them. Every June the universe is sorted into portfolios on size, book-to-market, operating profitability and asset growth using company financials from US Fundamental Data, momentum is ranked daily on the trailing year, and each factor is the value-weighted return of the long portfolios minus the short ones, priced off the US Equity Coarse Universe. The definitions are the ones published by the Kenneth R. French Data Library. The reason for computing them is timing: the library re-publishes on a monthly cycle, so its own values trail the present by about two months, which is too late to trade on. These arrive the day after the session they measure.
Because the inputs are different, these are close to the library's own numbers but not identical to them. Measured against the published daily factors:
| Factor | Correlation, 2000 to 2026 | Correlation, 2020 to 2026 | Beta |
|---|---|---|---|
| Mkt-RF | 0.995 | 0.999 | 0.98 |
| Momentum | 0.973 | 0.988 | 0.90 |
| HML | 0.956 | 0.980 | 0.90 |
| SMB | 0.949 | 0.988 | 0.95 |
| RMW | 0.819 | 0.931 | 0.98 |
| CMA | 0.815 | 0.968 | 0.88 |
The two columns differ because the input data gets materially better over time, and RMW and CMA are the ones that feel it. The recent column is the one live trading operates in. Use these factors to read a regime, to measure what your portfolio is exposed to, or to attribute your returns; if you need the exact published values for a paper, take them from the library. The IsEstimate property tells you which rows QuantConnect computed.
For more information about the Fama French dataset, including CLI commands and pricing, see the dataset listing.
About the Provider
The Kenneth R. French Data Library is maintained by Kenneth R. French at the Tuck School of Business at Dartmouth College. It is the canonical public source for the Fama-French factors, built from the CRSP and Compustat databases and refreshed as those databases update. The data is published free of charge.
QuantConnect computes the factors from its own fundamental and price data, to those same definitions, so they are delivered to your algorithm on the date each value became available, with no look-ahead.
Requesting Data
To add Fama-French Factors data to your algorithm, call the AddDataadd_data method. The dataset is unlinked, a single market-wide series, so instead of a security Symbol you pass the fixed identifier "FF". Save a reference to the dataset Symbol so you can access the data later in your algorithm.
The factors are a signal rather than a tradeable instrument, so add a separate tradeable security if you want to place orders.
class FamaFrenchDataAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2020, 1, 1)
self.set_end_date(2020, 12, 31)
self.set_cash(100000)
self.spy = self.add_equity("SPY", Resolution.DAILY).symbol
self.fama_french = self.add_data(FamaFrench, "FF", Resolution.DAILY).symbol public class FamaFrenchDataAlgorithm : QCAlgorithm
{
private Symbol _spy, _famaFrench;
public override void Initialize()
{
SetStartDate(2020, 1, 1);
SetEndDate(2020, 12, 31);
SetCash(100000);
_spy = AddEquity("SPY", Resolution.Daily).Symbol;
_famaFrench = AddData<FamaFrench>("FF", Resolution.Daily).Symbol;
}
}
Accessing Data
To get the current Fama-French Factors data, index the current Slice with the dataset Symbol. Slice objects deliver unique events to your algorithm as they happen, but the Slice may not contain data for your dataset at every time step. To avoid issues, check if the Slice contains the data you want before you index it.
def on_data(self, slice: Slice) -> None:
if slice.contains_key(self.fama_french):
factors = slice[self.fama_french]
self.log(f"Momentum at {slice.time}: {factors.momentum}") public override void OnData(Slice slice)
{
if (slice.ContainsKey(_famaFrench))
{
var factors = slice[_famaFrench];
Log($"Momentum at {slice.Time}: {factors.Momentum}");
}
}
Every factor is nullable, because the source marks a missing observation with a sentinel that the processor writes as an empty cell, so check for a missing value before you act on it. The Value property carries the value factor (HML), and the IsEstimate property tells you whether the row was published by the Ken French Data Library or reconstructed by QuantConnect.
Historical Data
To get historical Fama-French Factors data, call the History method with the dataset Symbol. If there is no data in the period you request, the history result is empty.
# DataFrame history_df = self.history(self.fama_french, 100, Resolution.DAILY) # Dataset objects history_bars = self.history[FamaFrench](self.fama_french, 100, Resolution.DAILY)
var history = History<FamaFrench>(_famaFrench, 100, Resolution.Daily);
For more information about historical data, see History Requests.
Example Applications
The Fama-French Factors dataset lets you measure and time your exposure to the classic equity style premiums. Examples include:
- Regressing a strategy's daily returns on the factors to split its performance into alpha and factor bets.
- Timing a broad equity position with the momentum factor, holding the risk only while the regime stays positive.
- Rotating between a value and a growth tilt on the recent trend in HML.
- Using the market excess return and the risk-free rate to compute Sharpe ratios and CAPM betas against your own equity curve.
Classic Algorithm Example
The following example algorithm holds the market while the Fama-French momentum factor is positive and stays in cash otherwise:
from AlgorithmImports import *
class FamaFrenchFactorsAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2020, 12, 31)
self.set_cash(100000)
self.spy = self.add_equity("SPY", Resolution.DAILY).symbol
self.fama_french = self.add_data(FamaFrench, "FF", Resolution.DAILY).symbol
history = self.history[FamaFrench](self.fama_french, 60, Resolution.DAILY)
self.debug(f"We got {len(list(history))} items from our history request")
def on_data(self, slice):
if not slice.contains_key(self.fama_french):
return
momentum = slice[self.fama_french].momentum
if momentum is None:
return
# Hold the market while the momentum factor is positive
if momentum > 0:
self.set_holdings(self.spy, 1)
# Step aside while it is negative
else:
self.liquidate(self.spy) public class FamaFrenchFactorsAlgorithm : QCAlgorithm
{
private Symbol _spy, _famaFrench;
public override void Initialize()
{
SetStartDate(2020, 1, 1);
SetEndDate(2020, 12, 31);
SetCash(100000);
_spy = AddEquity("SPY", Resolution.Daily).Symbol;
_famaFrench = AddData<FamaFrench>("FF", Resolution.Daily).Symbol;
var history = History<FamaFrench>(_famaFrench, 60, Resolution.Daily);
Debug($"We got {history.Count()} items from our history request");
}
public override void OnData(Slice slice)
{
if (!slice.ContainsKey(_famaFrench))
{
return;
}
var momentum = slice.Get<FamaFrench>(_famaFrench).Momentum;
if (!momentum.HasValue)
{
return;
}
// Hold the market while the momentum factor is positive
if (momentum > 0)
{
SetHoldings(_spy, 1);
}
// Step aside while it is negative
else
{
Liquidate(_spy);
}
}
}
Framework Algorithm Example
The following example algorithm uses the Algorithm Framework to trade a manually selected universe from Fama-French Factors data. An alpha model subscribes to the factors and emits insights from the sign of the momentum factor, holding the universe while the momentum regime is positive:
from AlgorithmImports import *
class FamaFrenchFrameworkAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2020, 1, 1)
self.set_end_date(2020, 12, 31)
self.set_cash(100000)
self.universe_settings.resolution = Resolution.DAILY
symbols = [Symbol.create("SPY", SecurityType.EQUITY, Market.USA)]
self.set_universe_selection(ManualUniverseSelectionModel(symbols))
self.add_alpha(FamaFrenchAlphaModel(self))
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
self.set_execution(ImmediateExecutionModel())
class FamaFrenchAlphaModel(AlphaModel):
def __init__(self, algorithm: QCAlgorithm) -> None:
self._tradable_symbols = []
# One subscription carries every factor, keyed by a fixed identifier
self._fama_french = algorithm.add_data(FamaFrench, "FF", Resolution.DAILY).symbol
history = algorithm.history[FamaFrench](self._fama_french, 60, Resolution.DAILY)
algorithm.debug(f"We got {len(list(history))} items from our history request")
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
if self._fama_french not in data:
return []
momentum = data[self._fama_french].momentum
if momentum is None:
return []
# A positive momentum factor is a risk-on regime, so hold the universe and step aside otherwise
direction = InsightDirection.UP if momentum > 0 else InsightDirection.FLAT
return [Insight.price(symbol, timedelta(days=7), direction) for symbol in self._tradable_symbols]
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
for security in changes.added_securities:
if security.symbol != self._fama_french:
self._tradable_symbols.append(security.symbol)
for security in changes.removed_securities:
if security.symbol in self._tradable_symbols:
self._tradable_symbols.remove(security.symbol) public class FamaFrenchFrameworkAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2020, 1, 1);
SetEndDate(2020, 12, 31);
SetCash(100000);
UniverseSettings.Resolution = Resolution.Daily;
var symbols = new[] { QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA) };
SetUniverseSelection(new ManualUniverseSelectionModel(symbols));
AddAlpha(new FamaFrenchAlphaModel(this));
SetPortfolioConstruction(new EqualWeightingPortfolioConstructionModel());
SetExecution(new ImmediateExecutionModel());
}
}
public class FamaFrenchAlphaModel : AlphaModel
{
private readonly List<Symbol> _tradableSymbols = new();
private readonly Symbol _famaFrench;
public FamaFrenchAlphaModel(QCAlgorithm algorithm)
{
// One subscription carries every factor, keyed by a fixed identifier
_famaFrench = algorithm.AddData<FamaFrench>("FF", Resolution.Daily).Symbol;
var history = algorithm.History<FamaFrench>(_famaFrench, 60, Resolution.Daily);
algorithm.Debug($"We got {history.Count()} items from our history request");
}
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
if (!data.ContainsKey(_famaFrench))
{
return Enumerable.Empty<Insight>();
}
var momentum = data.Get<FamaFrench>(_famaFrench).Momentum;
if (momentum == null)
{
return Enumerable.Empty<Insight>();
}
// A positive momentum factor is a risk-on regime, so hold the universe and step aside otherwise
var direction = momentum > 0 ? InsightDirection.Up : InsightDirection.Flat;
return _tradableSymbols.Select(symbol => Insight.Price(symbol, TimeSpan.FromDays(7), direction));
}
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var security in changes.AddedSecurities)
{
if (security.Symbol != _famaFrench)
{
_tradableSymbols.Add(security.Symbol);
}
}
foreach (var security in changes.RemovedSecurities)
{
_tradableSymbols.Remove(security.Symbol);
}
}
}