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

> Implement a value factor book-to-market strategy in QuantConnect. Filter equities by market cap, sort by P/B ratio, and take long/short positions in the cheapest/most expensive quintiles. Learn how to build this quant trading s...

- Repository: [Papers With Backtest/awesome-systematic-trading](https://github.com/paperswithbacktest/awesome-systematic-trading)
- Tags: how-to-guide
- Published: 2026-08-01

---

**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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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.

```python
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.

```python
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.

```python
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.

```python
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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/static/strategies/value-book-to-market-factor.py):

```python
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:

```bash
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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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.