In partnership with

A W-2, a Laundromat Owner & a Billionaire Walk Into a Room…

NOVEMBER 2-4 | AUSTIN, TX

“Almost no one in the history of the Forbes list has gotten there with a salary. You get rich by owning things.” –Sam Altman

At Main Street Over Wall Street 2025, you’ll learn the exact playbook we’ve used to help thousands of “normal” people find, fund, negotiate, and buy profitable businesses that cash flow.

  • Tactical business buying training and clarity

  • Relationships with business owners, investors, and skilled operators

  • Billionaire mental frameworks for unlocking capital and taking calculated risk

  • The best event parties you’ve ever been to

Use code BHP500 to save $500 on your ticket today (this event WILL sell out).

Click here to get your ticket, see the speaker list, schedule, and more

🚀 Your Investing Journey Just Got Better: Premium Subscriptions Are Here! 🚀

It’s been 4 months since we launched our premium subscription plans at GuruFinance Insights, and the results have been phenomenal! Now, we’re making it even better for you to take your investing game to the next level. Whether you’re just starting out or you’re a seasoned trader, our updated plans are designed to give you the tools, insights, and support you need to succeed.

Here’s what you’ll get as a premium member:

  • Exclusive Trading Strategies: Unlock proven methods to maximize your returns.

  • In-Depth Research Analysis: Stay ahead with insights from the latest market trends.

  • Ad-Free Experience: Focus on what matters most—your investments.

  • Monthly AMA Sessions: Get your questions answered by top industry experts.

  • Coding Tutorials: Learn how to automate your trading strategies like a pro.

  • Masterclasses & One-on-One Consultations: Elevate your skills with personalized guidance.

Our three tailored plans—Starter Investor, Pro Trader, and Elite Investor—are designed to fit your unique needs and goals. Whether you’re looking for foundational tools or advanced strategies, we’ve got you covered.

Don’t wait any longer to transform your investment strategy. The last 4 months have shown just how powerful these tools can be—now it’s your turn to experience the difference.

Markets aren’t always efficient. Stocks can be mispriced for extended periods. This creates opportunities if you know where to look.

Factor models can give us insights around these inefficiencies as they break expected stock returns into key drivers.

The Fama-French 5-Factor Model, for example, explains returns using market risk, size, value, profitability, and investment.

But even with these factors, some returns remain unexplained. That’s alpha, the portion of returns that can’t be justified by known risk factors.

End-to-end Implementation Python Notebook provided below.

This article, we’ll discuss:

  • Downloading and cleaning Fama-French factor data

  • Running rolling factor regressions to explain excess returns

  • Identify mispricing signals using statistical tests

  • Tracking factor exposures and return contributions over time

1. The 5-Factor Model + Momentum

Stock returns are influenced by multiple factors beyond overall market movements. That’s been made clear in financial literature.

The Fama-French 5-Factor Model expands on traditional asset pricing by incorporating five key drivers of returns:

Ri may be portfolio returns or individual stock returns.

Let’s discuss further.

Where Accomplished Peers Share What Really Works

Long Angle connects accomplished entrepreneurs and executives with institutional-grade investment opportunities. No membership fees. Access includes a private forum to exchange insights and ideas.

  • Confidential, peer-only discussions — no solicitation

  • Portfolio insights from active investors ($100M+ invested annually)

  • Proven strategies tested by experienced members

1.1 Market Risk (Rm−Rf​)

This factor represents the excess return of the market over the risk-free rate. It is calculated as:

A stock (or portfolio) with a high beta (>1) is more volatile than the market and a low beta (<1) stock is less sensitive to market movements.

1.2 Size Factor (SMB: Small Minus Big)

This factor captures the size premium since small-cap stocks tend to outperform large-cap stocks over time.

The factor construction is as follows:

  • Each month, all stocks ‘universe’ are sorted into small-cap (S) and large-cap (B) groups based on market capitalization.

  • Within each group, stocks are further split into value, neutral, and growth segments using book-to-market ratios.

  • SMB is then calculated as the average return of small-cap portfolios minus the average return of large-cap portfolios:

V, N, and G represent value, neutral, and growth portfolios.

A stock which has a positive SMB beta, means that it behaves like a small-cap stock. A negative SMB beta suggests large-cap characteristics.

1.3 Value Factor (HML: High Minus Low)

The value premium suggests that cheap stocks (high book-to-market) tend to outperform expensive stocks (low book-to-market).

  • Stocks are ranked by book-to-market ratio and divided into high (H), neutral (N), and low (L) groups.

  • HML is the average return of value stocks minus the average return of growth stocks:

A positive HML beta suggests a stock behaves like a value stock, and a negative HML beta indicates growth characteristics.

1.4 Profitability Factor (RMW: Robust Minus Weak)

Firms with high profitability tend to outperform firms with low profitability. The RMW factor measures this effect.

  • Stocks are ranked by operating profitability (revenues minus cost of goods sold, interest, and SG&A expenses, divided by total equity).

  • The return of high-profitability stocks is compared to low-profitability stocks:

R, Robust, represents firms with high profitability and W, Weak, represents low-profitability firms.

A high RMW beta suggests exposure to profitable firms, and a low or negative beta indicates sensitivity to low-profitability stocks.

1.5. Investment Factor (CMA: Conservative Minus Aggressive)

Firms that invest aggressively tend to underperform firms with conservative investment strategies. The CMA factor captures this effect.

  • Stocks are ranked by their asset growth rate (change in total assets over total assets).

  • The return of firms with low investment (conservative) is compared to high-investment firms (aggressive):

C, Conservative, represents firms with low investment, and A, Aggressive, represents firms with high investment.

A high CMA beta suggests the stock aligns with firms that invest conservatively. A low or negative beta indicates aggressive reinvestment behavior by the stock.

1.6. Momentum Factor (MOM: Winners Minus Losers)

Momentum suggests that stocks that have performed well in the past tend to keep outperforming in the short term.

  • Stocks are ranked by their returns over the past 12 months (excluding the most recent month).

  • The return of past winners (top 30%) is compared to the return of past losers (bottom 30%):

This factor accounts for trend persistence in stock prices, which traditional valuation-based factors do not capture.

A positive MOM beta indicates momentum exposure, i.e. stocks that trend upward. A negative beta suggests mean-reverting behavior.

Why This Matters

Each factor isolates a unique driver of stock/portfolio returns. If a stock’s performance aligns with these factors, it suggests exposure to known risks.

If it consistently outperforms beyond typical factor expectations, it may indicate alpha — potential mispricing in the framework we’ll present.

Next, we’ll download real-world factor data to test the model.

2. Downloading the Factor & Stock Price Data

2.1 Fama-French 5-factor Data

The Fama-French 5-factor data is hosted on Dartmouth’s Ken French Data Library. The dataset contains daily (or monthly/weekly) factor returns.

We download this data programmatically with the following Python code:

import requests
import zipfile
import io
import re
import pandas as pd
import yfinance as yf

class FamaFrenchDownloader:
    """Downloads and processes Fama-French 5-factor data (daily)."""

    FF5_URL = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_5_Factors_2x3_daily_CSV.zip"

    @staticmethod
    def download_ff5():
        """Fetches and extracts F-F 5-Factor data from the web."""
        response = requests.get(FamaFrenchDownloader.FF5_URL)
        with zipfile.ZipFile(io.BytesIO(response.content)) as z:
            file_name = z.namelist()[0]
            with z.open(file_name) as f:
                return f.read().decode("utf-8").splitlines()

    @staticmethod
    def parse_ff5_data():
        """Extracts and cleans F-F 5-Factor data."""
        lines = FamaFrenchDownloader.download_ff5()

        # Keep only data lines (starting with 8-digit dates)
        data_lines = [line for line in lines if re.match(r'^\s*\d{8}', line)]

        # Read into DataFrame
        df = pd.read_csv(io.StringIO("\n".join(data_lines)), sep=r"\s*,\s*", header=None, engine="python")

        # Assign column names
        df.columns = ["Date", "MKT_RF", "SMB", "HML", "RMW", "CMA", "RF"]

        # Convert date column
        df["Date"] = pd.to_datetime(df["Date"], format="%Y%m%d", errors="coerce")

        # Convert numeric columns
        df.iloc[:, 1:] = df.iloc[:, 1:].apply(pd.to_numeric, errors="coerce")

        return df

# Call function
ff5_df = FamaFrenchDownloader.parse_ff5_data()
ff5_df

Figure 1. Fama-French factor data, which includes market risk (MKT_RF), size (SMB), value (HML), profitability (RMW), investment (CMA), and risk-free rate (RF). These are the independent variables used in the in the factor model regression.

Financial News Keeps You Poor. Here's Why.

The scandalous truth: Most market news is designed to inform you about what already happened, not help you profit from what's coming next.

When CNBC reports "Stock XYZ surges 287%"—you missed it.

What you actually need:

  • Tomorrow's IPO calendar (not yesterday's launches)

  • Crowdfunding deals opening this week (not closed rounds)

  • What real traders are positioning for (not TV talking heads)

  • Economic data that moves markets (before it's released)

The financial media industrial complex profits from keeping you one step behind.

Stocks & Income flips this backwards. We focus entirely on forward-looking intel that helps you get positioned before the crowd, not informed after the move.

Stop chasing trades that happened already.

Start prepping for the next one.

Stocks & Income is for informational purposes only and is not intended to be used as investment advice. Do your own research.

2.2 The Momentum Factor

Momentum is not included in the standard Fama-French dataset but is available separately.

We can fetch and clean it similarly:

import requests
import zipfile
import io
import re
import pandas as pd

class MomentumDownloader:
    """Downloads and processes the Fama-French Momentum Factor data (daily)."""

    MOM_URL = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Momentum_Factor_daily_CSV.zip"

    @staticmethod
    def download_momentum():
        """Fetches and extracts the Momentum Factor data from the web."""
        response = requests.get(MomentumDownloader.MOM_URL)
        with zipfile.ZipFile(io.BytesIO(response.content)) as z:
            file_name = z.namelist()[0]
            with z.open(file_name) as f:
                return f.read().decode("utf-8").splitlines()

    @staticmethod
    def parse_momentum_data():
        """Extracts and cleans the Momentum Factor data (daily)."""
        lines = MomentumDownloader.download_momentum()

        # Keep only data lines (starting with 8-digit YYYYMMDD)
        data_lines = [line for line in lines if re.match(r'^\s*\d{8}', line)]

        # Read into DataFrame
        df = pd.read_csv(io.StringIO("\n".join(data_lines)), sep=r"\s*,\s*", header=None, engine="python")

        # Assign column names
        df.columns = ["Date", "MOM"]

        # Convert date column
        df["Date"] = pd.to_datetime(df["Date"], format="%Y%m%d", errors="coerce")

        # Convert MOM to numeric
        df["MOM"] = pd.to_numeric(df["MOM"], errors="coerce")

        # Drop invalid rows
        df.dropna(subset=["Date", "MOM"], inplace=True)

        return df

# Call function
mom_df = MomentumDownloader.parse_momentum_data()
mom_df

Figure 2. Momentum factor (MOM) data, which captures the return spread between past winners and losers. This extends the Fama-French 5-factor model to account for trend persistence.

2.3 Downloading Stock Price Data

With factor data ready, the next step is collecting historical stock prices.

We’ll get this data from Yahoo Finance, then compute daily returns, and finally prepare it for merging with factor returns.

We’ll use a simple portfolio of MSFT and AAPL as an example. Note that you’ll be able to explain more of variability in returns with larger portfolio.

import yfinance as yf
import pandas as pd

class StockDataFetcher:
    """Downloads and processes stock data from Yahoo Finance."""

    @staticmethod
    def get_stock_data(tickers, weights, start_date="1990-01-01"):
        """
        Fetch stock data for multiple tickers and compute weighted daily returns.

        Parameters:
        tickers (list): List of stock tickers (e.g., ["AAPL", "MSFT"])
        weights (list): List of weights (e.g., [0.3, 0.7]). Must match length of tickers.
        start_date (str): Start date for historical data.

        Returns:
        DataFrame: Contains Date, stock prices, and portfolio daily returns.
        """
        if len(tickers) != len(weights):
            raise ValueError("Tickers and weights must have the same length")

        # Fetch data using default auto_adjust=True.
        raw_df = yf.download(tickers, start=start_date, progress=False, group_by="ticker")

        # Process multi-ticker data.
        if isinstance(raw_df.columns, pd.MultiIndex):
            # Use 'Adj Close' if available, else use 'Close'
            level_vals = raw_df.columns.get_level_values(1)
            price_col = "Adj Close" if "Adj Close" in level_vals else "Close"
            price_cols = [col for col in raw_df.columns if col[1] == price_col]
            raw_df = raw_df[price_cols]
            raw_df.columns = [col[0] for col in raw_df.columns]
        else:
            # For single ticker downloads.
            price_col = "Adj Close" if "Adj Close" in raw_df.columns else "Close"
            raw_df = raw_df[[price_col]]
            raw_df.columns = tickers

        # Ensure the DataFrame columns follow the order of tickers.
        raw_df = raw_df[tickers]

        # Compute daily returns.
        returns_df = raw_df.pct_change() * 100

        # Compute weighted portfolio return.
        portfolio_return = returns_df.dot(weights)

        # Combine data.
        final_df = raw_df.copy()
        final_df["Portfolio Return"] = portfolio_return
        final_df.reset_index(inplace=True)

        return final_df

# Get Stock / Portfolio Data
tickers = ["AAPL", "MSFT"]
weights = [0.3, 0.7] # This is the weight of stock in the portfolio
stock_df = StockDataFetcher.get_stock_data(tickers, weights)
stock_df

Figure 3. Stock price data for AAPL and MSFT, along with portfolio returns computed based on their weighted allocation.

2.4 Merge Stock Returns with Factor Data

Now that we have both factor data and stock returns, the next step is merging them into a single dataset.

The full end-to-end workflow is available in a Google Colab notebook, exclusively for paid subscribers of my newsletter. Paid subscribers also gain access to the complete article, including the full code snippet in the Google Colab notebook, which is accessible below the paywall at the end of the article. Subscribe now to unlock these benefits! 👇

logo

Subscribe to our premium content to get access to the full article and code snippets.

Become a paying subscriber to get access to this post and other subscriber-only content.

Upgrade

Keep Reading

No posts found