, ,

Trading Moving Linear Regression R Squared (MLRR2) Explained with Python

Posted by

Updated May 2026: I’ve refreshed this MLRR2 guide to clarify what R-squared does and does not show, separate trend direction from fit quality, and modernise the Python walkthrough.

Chameleon watching market data used to introduce Moving Linear Regression R Squared
If you’re keeping an eye out for a congestion breakout can we use MLRR2 to boost our confidence in the outcome?
Table of Contents

    What Is Moving Linear Regression R Squared?

    Moving Linear Regression R Squared, usually shortened here to MLRR2, is a fit-quality indicator. It measures how closely recent prices line up with a straight regression line.

    That makes it different from the Moving Linear Regression line itself. MLR shows the fitted line. MLRR2 asks how well the recent price window fits that line.

    The value runs from 0 to 1.

    A value near 1 means the recent prices fit a straight line closely. That line could be rising or falling. R-squared does not tell you direction by itself.

    A value near 0 means the recent prices do not fit a straight line well. The window may be choppy, curved, noisy, or changing direction.

    That distinction is worth paying attention to. MLRR2 can tell you that the recent move is clean or messy, but you still need another tool, such as the MLR slope, price action, or a breakout signal, to tell you direction.

    In this article, I’m mostly interested in using MLRR2 beside the Congestion Count indicator. The idea is to see whether a congestion breakout is happening with a cleaner linear structure behind it, or whether the chart may be producing another weak break from a messy range.

    MLR vs MLRR2

    Moving Linear Regression and Moving Linear Regression R Squared are related, but they are not the same thing.

    MLR plots the rolling fitted regression line. It helps show whether the recent price window is leaning up, leaning down, or flattening.

    MLRR2 measures the quality of that fit. It asks whether prices in the window are lining up cleanly with a straight line or scattering around it.

    A rising MLR line with high MLRR2 may show a cleaner upward trend. A falling MLR line with high MLRR2 may show a cleaner downward trend. A high MLRR2 reading alone does not tell you whether the trend is up or down.

    That is why I would not read MLRR2 as a direction signal. I would read it as a confidence or cleanliness measure for the regression fit.

    ToolWhat it showsWhat it does not show
    MLRDirection and level of the fitted rolling regression lineHow good the fit is
    MLRR2How closely prices fit the rolling regression lineWhether the line is rising or falling
    Congestion CountHow long price has been overlapping inside a rangeWhether the breakout will follow through
    How MLR, MLRR2 and Congestion Count answer different questions

    How MLRR2 Is Calculated

    MLRR2 uses the same rolling price window as Moving Linear Regression. For each window, we fit a straight line through the recent prices and then measure how well that line explains the prices in that window.

    Before the formula, here are the symbols:

    yᵢ means an actual price in the window.

    ŷᵢ means the fitted price from the regression line.

    ȳ means the average price in the window.

    SSres means the residual sum of squares.

    SStot means the total sum of squares.

    The common regression version of R-squared is:

    R^2 = 1 - \frac{SS_{res}}{SS_{tot}}

    R-squared = 1 – unexplained variation / total variation

    SSres measures the squared errors between the actual prices and the fitted regression line.

    SS_{res} = \sum (y_i - \hat{y}_i)^2

    Unexplained variation = sum of squared gaps between actual prices and fitted prices

    SStot measures the squared distance between the actual prices and their average.

    SS_{tot} = \sum (y_i - \bar{y})^2

    Total variation = sum of squared gaps between actual prices and their average

    If the fitted line explains most of the price movement in the window, R-squared is close to 1. If the fitted line explains very little, R-squared is closer to 0.

    A high MLRR2 value does not automatically mean “buy” or “sell.” It means the recent prices fit a straight line well. The line could be rising, falling, or flat. Direction has to come from the MLR line, price action, or another signal.

    For a congestion-breakout setup, the useful question is whether MLRR2 is rising as price leaves the range. If it is, the breakout may be developing into a cleaner directional move. If MLRR2 stays low, the breakout may be less convincing.

    Why Combine MLRR2 with Congestion Count?

    The Congestion Count indicator looks for overlapping price action. It is trying to spot when price has spent several bars trapped inside a shared range.

    That can be useful, but a congestion break is not automatically a good trade. Some breaks follow through. Some fail almost immediately. Some only move sideways into a new small range.

    MLRR2 gives us a different piece of information. It does not count the congestion. It measures whether recent prices are lining up cleanly with a straight regression line.

    The idea in this article is to combine the two. Congestion Count marks the build-up and break. MLRR2 helps judge whether price action around that break is becoming more linear and directional.

    This is not a finished trading system. It is a way to add another diagnostic layer to the congestion-breakout idea.

    Coding MLRR2 and Congestion Count in Python

    Now we can build the chart in Python.

    This tutorial adds MLRR2 to the Congestion Count chart. The Congestion Count gives us the overlapping-price-range idea, while MLRR2 tells us whether recent prices are fitting a straight line cleanly.

    I am going to keep this as a step-by-step build rather than one large code dump. Each Python-labelled block goes into the same file, in order.

    Step 1: Install the Python libraries

    This guide assumes you already have Python installed and are using Visual Studio Code.

    Open a terminal in VS Code by clicking Terminal > New Terminal, then run:

    Bash
    python -m pip install pandas yfinance mplfinance matplotlib numpy

    On some Windows machines, this version works instead:

    Bash
    py -m pip install pandas yfinance mplfinance matplotlib numpy

    Step 2: Create the file and import the libraries

    Create a new Python file and save it as:

    congestion_mlrr2.py

    At the top of the file, add:

    Python
    import pandas as pd
    import yfinance as yf
    import mplfinance as mpf
    import numpy as np
    from matplotlib.lines import Line2D

    pandas stores the price data in a table.

    yfinance downloads the market data.

    mplfinance draws the candlestick chart.

    numpy helps with the regression and R-squared calculations.

    Line2D lets us create a clean legend for the chart.

    Step 3: Add the settings

    Next, add the settings near the top of the file. Keeping these values together makes the script easier to change later.

    Python
    ticker = "TSLA"
    chart_title = "Tesla"
    
    start_date = "2025-05-01"
    end_date = "2026-05-01"
    
    lookback = 7

    ticker is the Yahoo Finance symbol.

    chart_title is the title printed on the chart.

    The dates use YYYY-MM-DD format. yfinance treats the end date as a cut-off, so you can push it one trading day later if you want the most recent available bar included.

    lookback controls both the Congestion Count window and the MLRR2 regression window in this example.

    Step 4: Download the market data

    Python
    df = yf.download(
        ticker,
        start=start_date,
        end=end_date,
        auto_adjust=True,
        progress=False,
        multi_level_index=False
    )
    
    if df.empty:
        raise RuntimeError("No data was downloaded. Check the ticker symbol and date range.")
    
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)
    
    df.index = pd.DatetimeIndex(df.index)
    

    This downloads open, high, low, close and volume data.

    auto_adjust=True keeps the price series adjusted where applicable.

    progress=False removes the download progress bar.

    multi_level_index=False keeps the columns easier to work with.

    The final line makes sure the index is treated as dates, which mplfinance expects when plotting.

    Step 5: Define the Congestion Count function

    For this tutorial I’m using a simplified Congestion Count proxy. It checks whether the current bar remains inside the recent lookback range. The dedicated Congestion Count article uses the fuller overlapping-zone logic, so treat this version as a compact teaching example rather than a perfect duplicate of that script.

    Now define the Congestion Count function.

    This version checks whether the current high and low remain inside the recent high-low range. If they do, the congestion count increases. If price breaks outside that range, the count resets.

    Python
    def congestion_count(data, lookback=7):
        count = pd.Series(0, index=data.index, dtype="int64")
    
        for i in range(lookback, len(data)):
            recent_low = data["Low"].iloc[i - lookback:i].min()
            recent_high = data["High"].iloc[i - lookback:i].max()
    
            current_low = data["Low"].iloc[i]
            current_high = data["High"].iloc[i]
    
            if current_low >= recent_low and current_high <= recent_high:
                count.iloc[i] = count.iloc[i - 1] + 1
            else:
                count.iloc[i] = 0
    
        return count

    recent_low and recent_high define the recent range.

    If the current bar stays inside that range, the count rises.

    If price breaks outside the range, the count goes back to zero.

    Step 6: Define the MLRR2 function

    Next we calculate MLRR2.

    For each rolling window, the function fits a straight line through the closing prices and then calculates R-squared. A higher value means the prices in that window fit a straight line more cleanly.

    Python
    def calculate_mlrr2(data, lookback=7, price="Close"):
        values = pd.Series(np.nan, index=data.index, dtype="float64")
    
        x = np.arange(lookback)
    
        for i in range(lookback - 1, len(data)):
            y = data[price].iloc[i - lookback + 1:i + 1].values
    
            slope, intercept = np.polyfit(x, y, 1)
            fitted = intercept + slope * x
    
            ss_res = np.sum((y - fitted) ** 2)
            ss_tot = np.sum((y - y.mean()) ** 2)
    
            if ss_tot == 0:
                values.iloc[i] = np.nan
            else:
                values.iloc[i] = 1 - (ss_res / ss_tot)
    
        return values.clip(lower=0, upper=1)

    x is the bar number inside the rolling window.

    y is the group of closing prices inside that same window.

    np.polyfit fits the straight line.

    ss_res measures the squared gaps between actual prices and the fitted line.

    ss_tot measures the squared gaps between actual prices and their average.

    The final value is R-squared, clipped between 0 and 1.

    Step 7: Add the indicators to the DataFrame

    Now add both indicators to the price table.

    Python
    df["Congestion Count"] = congestion_count(df, lookback=lookback)
    df["MLRR2"] = calculate_mlrr2(df, lookback=lookback)
    
    plot_data = df.dropna(subset=["MLRR2"])

    Think of df as a spreadsheet. It already has Open, High, Low, Close and Volume columns. This step adds Congestion Count and MLRR2 as two new columns.

    Step 8: Define the indicator plots

    Next we tell mplfinance which extra lines to draw below the price chart.

    Python
    indicator_plots = [
        mpf.make_addplot(
            plot_data["Congestion Count"],
            panel=2,
            color="blue",
            width=1.0,
            secondary_y=False,
            ylabel="Congestion Count"
        ),
        mpf.make_addplot(
            plot_data["MLRR2"],
            panel=2,
            color="red",
            width=1.0,
            secondary_y=True
        )
    ]

    The blue line is the Congestion Count.

    The red line is MLRR2.

    They are plotted in the same lower panel, but MLRR2 uses a secondary y-axis because its values run from 0 to 1 while the congestion count can rise above that.

    Step 9: Create the chart

    Now create the candlestick chart.

    Python
    fig, axes = mpf.plot(
        plot_data,
        type="candle",
        style="yahoo",
        volume=True,
        addplot=indicator_plots,
        panel_ratios=(3, 1, 1.4),
        title=f"{chart_title} with Congestion Count and MLRR2",
        figsize=(11, 7),
        returnfig=True
    )
    
    fig.subplots_adjust(
        left=0.07,
        right=0.90,
        top=0.92,
        bottom=0.16,
        hspace=0.05
    )

    type="candle" gives us the candlestick chart.

    volume=True adds the volume panel.

    addplot=indicator_plots adds the Congestion Count and MLRR2 lines.

    panel_ratios controls the relative size of the price, volume and indicator panels.

    subplots_adjust gives the chart extra room so labels and angled dates are not clipped.

    Step 10: Add congestion shading and breakout arrows

    The next block adds the visual annotations. It shades congestion bars and marks breaks from congestion with arrows.

    Python
    price_axis = axes[0]
    
    for i in range(len(plot_data)):
        count_value = int(plot_data["Congestion Count"].iloc[i])
    
        if count_value > 0:
            x_pos = plot_data.index.get_loc(plot_data.index[i])
            low = plot_data["Low"].iloc[i]
            high = plot_data["High"].iloc[i]
    
            # Shade the bar while congestion is active
            price_axis.fill_between(
                [x_pos - 0.5, x_pos + 0.5],
                low,
                high,
                color="black",
                alpha=0.15
            )
    
            # Add the count label only when the count increases
            if i > 0 and count_value > plot_data["Congestion Count"].iloc[i - 1]:
                price_axis.text(
                    x_pos,
                    low,
                    str(count_value),
                    color="blue",
                    fontsize=7,
                    ha="center",
                    va="top"
                )
    
        # If congestion just ended, check whether price broke above or below the range
        if i > 0:
            previous_count = int(plot_data["Congestion Count"].iloc[i - 1])
            current_count = int(plot_data["Congestion Count"].iloc[i])
    
            if previous_count > 0 and current_count == 0:
                x_pos = plot_data.index.get_loc(plot_data.index[i])
    
                start = max(i - previous_count, 0)
                congestion_low = plot_data["Low"].iloc[start:i].min()
                congestion_high = plot_data["High"].iloc[start:i].max()
    
                close = plot_data["Close"].iloc[i]
                low = plot_data["Low"].iloc[i]
                high = plot_data["High"].iloc[i]
                bar_range = high - low if high != low else 1
    
                if close > congestion_high:
                    # Breakout above congestion
                    arrow_y = low
                    arrow_dy = -0.08 * bar_range
                    arrow_color = "green"
                elif close < congestion_low:
                    # Breakdown below congestion
                    arrow_y = high
                    arrow_dy = 0.08 * bar_range
                    arrow_color = "red"
                else:
                    continue
    
                arrow_size = max(previous_count / 8, 0.6)
    
                price_axis.annotate(
                    "",
                    xy=(x_pos, arrow_y),
                    xytext=(x_pos, arrow_y + arrow_dy),
                    arrowprops=dict(
                        facecolor=arrow_color,
                        edgecolor=arrow_color,
                        shrink=0.05,
                        headlength=10 * arrow_size,
                        headwidth=10 * arrow_size
                    )
                )

    This block is the most visual part of the script.

    When Congestion Count is above zero, the bar is shaded.

    When congestion ends, the code checks whether price closed above or below the congestion range.

    A green arrow marks an upside break.

    A red arrow marks a downside break.

    The arrow size grows with the previous congestion count.

    Step 11: Add the legend and show the chart

    Finally, add a legend, save the image and show the chart.

    Python
    legend_items = [
        Line2D([], [], color="blue", label="Congestion Count"),
        Line2D([], [], color="red", label="MLRR2"),
        Line2D([], [], color="black", alpha=0.3, linewidth=6, label="Congestion zone"),
        Line2D([], [], color="green", marker="^", linestyle="None", label="Upside break"),
        Line2D([], [], color="red", marker="v", linestyle="None", label="Downside break")
    ]
    
    fig.legend(handles=legend_items, loc="center left", bbox_to_anchor=(0.04, 0.5))
    
    fig.savefig("congestion_mlrr2_chart.png", dpi=150, bbox_inches="tight")
    
    mpf.show()

    fig.legend() adds a chart legend.

    fig.savefig() saves the chart as a PNG image in the same folder as the script.

    mpf.show() opens the finished chart window.

    Step 12: Run the script

    Save congestion_mlrr2.py by pressing Ctrl+S.

    The easiest route is usually to click the play button in the top-right corner of VS Code. If that works, the chart window should open.

    You can also run it from the terminal. Open Terminal > New Terminal, move into the folder where you saved the script, then run:

    Bash
    python congestion_mlrr2.py

    On some Windows setups, use:

    Bash
    py congestion_mlrr2.py

    Your chart should show Tesla candlesticks, volume, congestion shading, breakout arrows, and a lower panel with Congestion Count and MLRR2.

    VS Code screenshot showing a Python-generated Tesla candlestick chart with Congestion Count and MLRR2 indicators
    Tesla chart with Congestion Count in blue and MLRR2 in red, plotted from the Python script. The shaded areas show congestion periods, while the arrows mark breaks from those ranges.

    How to Interpret the MLRR2 and Congestion Count Chart

    The red line in the lower panel is MLRR2. It runs from 0 to 1. Higher readings mean recent prices are fitting a straight regression line more closely. Lower readings mean the recent price action is messier or less linear.

    The blue line is the Congestion Count. It rises when price keeps overlapping inside the recent range, and resets when price breaks outside that range.

    The useful part is comparing the two. A high congestion count tells us price has been coiling or overlapping. A rising MLRR2 reading can suggest price is beginning to move in a cleaner straight-line path. A low or erratic MLRR2 reading warns that the break may not have much structure behind it.

    A high MLRR2 value is not bullish or bearish by itself. It can rise during a rally or a sell-off. Direction still has to come from price action, the breakout, or the MLR slope.

    Reading the MLRR2 and Congestion Count Chart

    Annotated Tesla chart showing Congestion Count, MLRR2, congestion zones and breakout markers
    Annotated Tesla chart showing Congestion Count and MLRR2. The examples are deliberately mixed: the chart shows that MLRR2 can help judge whether price action is becoming more linear, but it does not turn every congestion break into a clean signal.

    This chart is useful, but not because it gives us a perfect textbook signal. It shows the messier reality of combining Congestion Count with MLRR2.

    On the left, the Congestion Count starts to appear, but MLRR2 is uneven. That suggests price is overlapping, but the recent closes are not yet fitting a clean linear move.

    During the stronger rally, MLRR2 rises while price is moving higher. In that section, the red line suggests the move is becoming more linear, rather than just breaking from congestion.

    In the middle of the chart, the count and MLRR2 both become more erratic. This is the kind of area where a congestion-breakout trader could get chopped up if every break is treated as equal.

    On the right, MLRR2 rises again during a falling move. This is the key reminder: a high MLRR2 reading is not bullish or bearish by itself. It means the recent prices fit a straight line more cleanly. Direction still has to come from price action, the MLR slope, or the breakout itself.

    Key Takeaways

    • MLRR2 measures how closely recent prices fit a straight regression line.
    • A high MLRR2 reading means the recent price window is more linear. It does not tell you whether the move is up or down.
    • A low MLRR2 reading means the recent prices are not fitting a straight line well.
    • Congestion Count and MLRR2 answer different questions. Congestion Count looks for overlapping price action. MLRR2 looks at fit quality.
    • Combining the two can help judge whether a congestion break is becoming cleaner or whether the chart remains messy.
    • This article uses a simplified Congestion Count proxy for the Python tutorial. The fuller overlapping-zone version is covered in the dedicated Congestion Count guide.
    • The examples are deliberately mixed. MLRR2 can add context, but it does not turn every congestion break into a clean trade.
    • Python makes it possible to test the idea across more tickers, timeframes and parameter settings.