How to Implement a Value Factor (Book-to-Market) Strategy in QuantConnect

You can implement a classic value (book-to-market) factor strategy by filtering the top 3,000 U.S. equities by market cap, sorting them by price-to-book ratio, and taking equal-weight long positions in the cheapest quintile while shorting the most expensive quintile.

The awesome-systematic-trading repository provides a curated collection of quantitative trading resources and ready-to-run QuantConnect strategy implementations. Its modular architecture contains over 40 Python scripts in static/strategies/, each demonstrating classic factor investing approaches including the high-minus-low (HML) value factor. This guide walks through the exact implementation details found in the repository's value factor script, showing you how to replicate the Fama-French book-to-market approach in the QuantConnect Lean engine.

Repository Architecture Overview

The repository organizes its strategy implementations as self-contained Python files that inherit from QCAlgorithm. Each script follows a consistent scaffold: importing from AlgorithmImports, defining universe selection methods, and scheduling periodic rebalancing. The value factor implementation resides in static/strategies/value-book-to-market-factor.py, which demonstrates a 5x leveraged HML portfolio with monthly rebalancing and custom transaction cost modeling.

Value Factor Implementation Details

The script constructs a long-short portfolio based on price-to-book (P/B) ratios, following the classic Fama-French HML methodology.

Universe Selection Logic

The strategy begins with a coarse universe filter to improve computational efficiency. In CoarseSelectionFunction, the algorithm retains only U.S. equities with fundamental data available, using a flag system to trigger updates only during rebalancing periods.

def CoarseSelectionFunction(self, coarse):
    if not self.selection_flag:
        return Universe.Unchanged
    return [c.Symbol for c in coarse
            if c.HasFundamentalData and c.Market == 'usa']

The self.coarse_count = 3000 parameter limits the universe to the 3,000 most capitalized stocks, ensuring liquidity and reducing survivorship bias.

Fine Selection and Quintile Sorting

The FineSelectionFunction implements the core value logic. It first filters for stocks with valid P/B ratios listed on major exchanges (NYS, NAS, ASE), then sorts by market capitalization to apply the coarse count limit.

def FineSelectionFunction(self, fine):
    eligible = [f for f in fine
                if f.ValuationRatios.PBRatio != 0 and
                   f.SecurityReference.ExchangeId in ("NYS", "NAS", "ASE")]
    
    top_by_cap = sorted(eligible,
                        key=lambda f: f.MarketCap,
                        reverse=True)[:self.coarse_count]
    
    sorted_by_pb = sorted(top_by_cap,
                          key=lambda f: f.ValuationRatios.PBRatio)
    
    quintile = len(sorted_by_pb) // 5
    self.long = [s.Symbol for s in sorted_by_pb[:quintile]]
    self.short = [s.Symbol for s in sorted_by_pb[-quintile:]]
    return self.long + self.short

The bottom quintile (lowest P/B) becomes the long basket, while the top quintile (highest P/B) becomes the short basket.

Rebalancing Schedule

The strategy executes monthly at month-end using QuantConnect's scheduling API. The Selection method toggles a boolean flag every 12 months to trigger universe updates.

def Initialize(self):
    self.Schedule.On(self.DateRules.MonthEnd(self.symbol),
                     self.TimeRules.AfterMarketOpen(self.symbol),
                     self.Selection)

def Selection(self):
    if self.month == 12:
        self.selection_flag = True
    self.month = (self.month % 12) + 1

Position Sizing and Leverage

Within OnData, the strategy liquidates any holdings that exited the selected universe, then applies equal-weight allocation to both sides. The implementation uses 5× leverage via SetLeverage(5) to amplify the factor exposure.

def OnData(self, data):
    if not self.selection_flag:
        return
    self.selection_flag = False
    
    for held in [x.Key for x in self.Portfolio if x.Value.Invested]:
        if held not in self.long + self.short:
            self.Liquidate(held)
    
    for sym in self.long:
        self.SetHoldings(sym, 1.0 / len(self.long))
    
    for sym in self.short:
        self.SetHoldings(sym, -1.0 / len(self.short))

A custom fee model charges 0.5 basis points per trade to simulate realistic transaction costs.

Complete Strategy Implementation

Here is the full annotated implementation from static/strategies/value-book-to-market-factor.py:

from AlgorithmImports import *

class Value(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100_000)
        
        self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.coarse_count = 3000
        self.long, self.short = [], []
        self.month = 12
        self.selection_flag = False
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction,
                         self.FineSelectionFunction)
        
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol),
                         self.TimeRules.AfterMarketOpen(self.symbol),
                         self.Selection)
    
    def CoarseSelectionFunction(self, coarse):
        if not self.selection_flag:
            return Universe.Unchanged
        return [c.Symbol for c in coarse
                if c.HasFundamentalData and c.Market == 'usa']
    
    def FineSelectionFunction(self, fine):
        eligible = [f for f in fine
                    if f.ValuationRatios.PBRatio != 0 and
                       f.SecurityReference.ExchangeId in ("NYS", "NAS", "ASE")]
        
        top_by_cap = sorted(eligible,
                            key=lambda f: f.MarketCap,
                            reverse=True)[:self.coarse_count]
        
        sorted_by_pb = sorted(top_by_cap,
                              key=lambda f: f.ValuationRatios.PBRatio)
        
        quintile = len(sorted_by_pb) // 5
        self.long = [s.Symbol for s in sorted_by_pb[:quintile]]
        self.short = [s.Symbol for s in sorted_by_pb[-quintile:]]
        return self.long + self.short
    
    def OnData(self, data):
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        for held in [x.Key for x in self.Portfolio if x.Value.Invested]:
            if held not in self.long + self.short:
                self.Liquidate(held)
        
        for sym in self.long:
            self.SetHoldings(sym, 1.0 / len(self.long))
        
        for sym in self.short:
            self.SetHoldings(sym, -1.0 / len(self.short))
        
        self.long.clear()
        self.short.clear()
    
    def Selection(self):
        if self.month == 12:
            self.selection_flag = True
        self.month = (self.month % 12) + 1

class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Running the Strategy Locally

To execute this value factor strategy in a local Lean installation:

git clone https://github.com/paperswithbacktest/awesome-systematic-trading.git
cd awesome-systematic-trading

cp static/strategies/value-book-to-market-factor.py /path/to/Lean/Algorithm

docker run -v /path/to/Lean:/Lean quantconnect/lean:latest \
    python /Lean/Algorithm/value-book-to-market-factor.py

Replace /path/to/Lean with your actual Lean engine installation directory. The script requires access to QuantConnect's fundamental data feed for P/B ratio calculations.

Summary

  • The awesome-systematic-trading repository provides a production-ready implementation of the classic HML value factor in static/strategies/value-book-to-market-factor.py.
  • The strategy selects the top 3,000 stocks by market cap, then sorts by P/B ratio to create quintile-based long-short portfolios.
  • Monthly rebalancing occurs at month-end using QuantConnect's scheduling API, with equal-weight allocation and 5× leverage applied to both sides.
  • A custom fee model simulates realistic 0.5 bps transaction costs, making backtests more representative of live trading performance.

Frequently Asked Questions

How does the value factor script handle data look-ahead bias?

The implementation avoids look-ahead bias by using the CoarseSelectionFunction and FineSelectionFunction workflow, which only accesses fundamental data available at the current algorithm time. The selection_flag ensures universe updates occur only after the month-end schedule triggers, preventing future information from influencing current portfolio construction.

What exchanges are included in the universe filter?

The fine selection method explicitly filters for securities listed on the New York Stock Exchange (NYS), NASDAQ (NAS), and American Stock Exchange (ASE). This filter ensures liquid, regulated U.S. equities while excluding OTC and pink sheet stocks that might distort the book-to-market calculations.

Can I modify the leverage or rebalancing frequency?

Yes. You can adjust the leverage by changing security.SetLeverage(5) to your desired multiple, though you must implement this in the OnSecuritiesChanged method or manually via SetHoldings. For rebalancing frequency, modify the Selection method logic—currently it triggers every 12 months (annually), but you could change the modulo operation to implement quarterly or semi-annual rebalancing.

Why does the strategy use price-to-book ratio instead of book-to-market?

The script technically uses P/B ratio (price-to-book) as the sorting metric, which is the inverse of book-to-market. Sorting by ascending P/B ratio is mathematically equivalent to sorting by descending book-to-market, so the lowest P/B quintile represents the highest book-to-market (value) stocks. This approach aligns with QuantConnect's ValuationRatios data structure while maintaining the classic Fama-French HML construction.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →