Unlocking Alpha: Harnessing Relative Strength for AI-Driven Stock Selection

Introduction

Hello folks! If you’ve been following along, we’ve covered a lot of ground already:

Now, we’re about to level up again. This post will explore one of the most crucial factors in my trading success and that of many others: relative strength. As always, here’s the updated repo.

What is Relative Strength?

Relative strength (RS) measures a stock’s performance compared to the overall market or its peers. It’s a key component of William O’Neil’s CANSLIM investment strategy. O’Neil developed CANSLIM to find market-leading stocks before they make significant price advances. The “L” in CANSLIM refers to “Leader or Laggard”. O’Neil would use RS to identify and rank these leading stocks by their performance.

Why is RS critical for traders? Because the market’s biggest winners typically have an RS rank of 80 or higher before their major moves. Strong RS signals institutional buying, a key driver of stock prices. Even legendary traders like Jesse Livermore and Nicolas Darvas, who preceded O’Neil, incorporated elements of relative strength in their strategies. Livermore famously looked for stocks “acting stronger than the market itself,” while Darvas focused on stocks hitting new highs and outperforming the broader market using his box technique. These early pioneers understood that stocks with superior relative performance often continue to outperform, a concept that O’Neil later formalized and quantified with his relative strength ranking system. By focusing on stocks with high relative strength, traders align themselves with the market’s leaders and position themselves for potential outsized gains in bullish market conditions.

Calculating Relative Strength

Here’s how we calculate RS:

  1. Measure a stock’s price change over the past 12 months
  2. Compare that to a benchmark index like the S&P 500
  3. Assign a percentile rank from 1 (worst) to 99 (best)

Let’s code it up in Python using OpenBB, pandas, and numpy:

Python
from typing import List
from datetime import datetime, timedelta

from langchain_core.pydantic_v1 import BaseModel, Field
from langchain.agents import tool
from openbb import obb

import pandas as pd
import numpy as np

from app.tools.utils import wrap_dataframe


def fetch_stock_data(
    symbol: str, start_date: datetime, end_date: datetime
) -> pd.DataFrame:
    return obb.equity.price.historical(
        symbol,
        start_date=start_date.strftime("%Y-%m-%d"),
        end_date=end_date.strftime("%Y-%m-%d"),
        provider="yfinance",
    ).to_df()


def fetch_sp500_data(start_date: datetime, end_date: datetime) -> pd.DataFrame:
    return obb.equity.price.historical(
        "^GSPC",
        start_date=start_date.strftime("%Y-%m-%d"),
        end_date=end_date.strftime("%Y-%m-%d"),
        provider="yfinance",
    ).to_df()


def calculate_performance(data: pd.DataFrame) -> float:
    """
    Calculate the performance of a stock over the given data period.
    """
    start_price = data["close"].iloc[0]
    end_price = data["close"].iloc[-1]
    performance = (end_price - start_price) / start_price
    return performance


def calculate_rs_rating(
    symbol: str, intervals: List[int], scaling_factor: int = 50
) -> pd.DataFrame:
    """
    Calculates the relative strength rating for a given stock symbol.

    The relative strength is based on the stock's performance compared to the S&P 500 index,
    measured at the specified intervals (market sessions).
    """
    end_date = datetime.now()
    rs_ratings = []

    for interval in intervals:
        start_date = end_date - timedelta(days=interval)

        stock_data = fetch_stock_data(symbol, start_date, end_date)
        sp500_data = fetch_sp500_data(start_date, end_date)

        stock_data.index = pd.to_datetime(stock_data.index)
        sp500_data.index = pd.to_datetime(sp500_data.index)

        stock_performance = calculate_performance(stock_data)
        sp500_performance = calculate_performance(sp500_data)

        # Calculate relative performance to S&P 500 and apply scaling factor
        relative_performance = (stock_performance - sp500_performance) * scaling_factor

        # Normalize the relative performance to a 1-99 score
        # Assuming the distribution of relative performances is known and we aim for a midpoint of 50
        # This part may need adjustment based on a universe of empirical data rather than a simple benchmark
        scaled_score = np.clip(relative_performance + 50, 1, 99)

        rs_ratings.append(scaled_score)

    rs_df = pd.DataFrame({"Interval": intervals, "RS_Rating": rs_ratings})
    return rs_df

This code does the following:

  • Defines a function calculate_rs_rating that takes a stock symbol, a list of intervals (in market sessions), and an optional scaling factor.
  • For each interval:
    • Fetches historical price data for the stock and S&P 500 index using the fetch_stock_data and fetch_sp500_data functions, respectively.
    • Calculates the performance of the stock and S&P 500 index using the calculate_performance function.
    • Computes the relative performance by subtracting the S&P 500 performance from the stock performance and multiplying by the scaling factor.
    • Normalizes the relative performance to a score between 1 and 99, assuming a midpoint of 50.
  • Returns a DataFrame containing the intervals and corresponding relative strength ratings.

Integrating Relative Strength into our AI Agent

Now that we can calculate and rank relative strength, let’s incorporate it into our AI stock analysis agent. We’ll update our StockStatsInput model and add a new tool:

Python
from langchain.agents import tool
from app.tools.utils import wrap_dataframe

class StockStatsInput(BaseModel):
    symbol: str = Field(..., description="The stock symbol to analyze")


@tool(args_schema=StockStatsInput)
def get_relative_strength(symbol: str) -> str:
    """Calculate relative strength for a list of stocks."""

    session_intervals = [21, 63, 126, 189, 252]

    try:
        rs_rating = calculate_rs_rating(symbol, session_intervals)
        return wrap_dataframe(rs_rating)
    except Exception as e:
        return f"\n<observation>\nError: {e}</observation>\n"
  • Defines a LangChain tool get_relative_strength using the @tool decorator and the StockStatsInput schema:
    • Calls the calculate_rs_rating function with predefined session intervals (21, 63, 126, 189, 252) to calculate the relative strength rating for the given stock symbol.
    • Returns the relative strength rating as a formatted string wrapped in <observation> tags.
    • If an exception occurs during the calculation, returns an error message wrapped in <observation> tags.

LangChain Tool Calling Updates

The fantastic folks at LangChain continue to push the limits of what is possible using function calling against Claude 3 Opus. This past week, that integration got even better with improved function calling and a new tool’s agent constructor. Let’s adapt our agent pipeline to the new changes.

Python
from langchain.agents import AgentExecutor, create_tool_calling_agent
# other imports
from app.tools.stock_relative_strength import get_relative_strength

SYSTEM_TEMPLATE = """
...
CRITERIA FOR BULLISH SETUPS:
----------------------------  
...
12. Stock's relative strength rank is above 80.
...
"""

def get_prompt():
    return ChatPromptTemplate.from_messages(
        [
            ("user", SYSTEM_TEMPLATE),
            ("placeholder", "{chat_history}"),
            ("human", "{input}"),
            ("placeholder", "{agent_scratchpad}"),
        ]
    )

def get_tools(llm):
    tavily = TavilySearchResults(max_results=1)

    tools = [
        get_relative_strength,
        get_valuation_multiples,
        get_stock_price_history,
        get_stock_quantstats,
        get_gainers,
        get_losers,
        get_stock_sector_info,
        get_news_sentiment,
        get_stock_ratios,
        get_key_metrics,
        tavily,
    ]
    return tools

def create_anthropic_agent_executor():
    llm = ChatAnthropic(
        temperature=0,
        model_name="claude-3-opus-20240229",
        max_tokens=4096,
    )

    tools = get_tools(llm)
    prompt = get_prompt()

    agent = create_tool_calling_agent(llm, tools, prompt)

    return AgentExecutor.from_agent_and_tools(
        agent=agent,
        tools=tools,
        verbose=True,
        return_intermediate_steps=True,
        handle_parsing_errors=True,
    )

I’ve integrated the new create_tool_calling_agent constructor and cleaned up the template using more granular messaging. Finally, I’ve added the relative strength criteria to the prompt template.

It’s worth noting that we can further refine our criteria by only considering stocks with an RS Rank above 90. Personally, I prefer to focus on the cream of the crop – the stocks with the absolute best relative strength. By setting a higher threshold, we can narrow our focus to the market’s true leaders and potentially increase our chances of finding stocks poised for significant gains. Of course, the exact RS Rank threshold can be adjusted based on your individual trading style and risk tolerance.

Example Usage

Let’s see our updated agent in action! We’ll have it analyze a current market leader by relative strength.

As you can see, the agent calculates the relative strength and returns an assessment. Now, astute traders and stock market pros will immediately notice that these RS values are not the same ones you will find in the wild. Why? Proprietary calculations and methodologies, my friends. Between firms, expect to see variation and interpretation. All I need to know is that…it’s high, very high. 🤖

The Road Ahead

Should you use RS as the final word in stock selection? Not exactly. As I’ve emphasized throughout this series, the best AI trading agents synthesize multiple factors, like fundamentals, technicals, trends, sentiment, and more. But if you want to trade like the market wizards, relative strength must be a key part of our AI agent’s tool belt to find stocks that fly higher and exceed any market index.

What’s next? Believe it or not, we can still develop our stock analysis engine further, and risk management is a crucial area to explore. While relative strength helps us identify potential winners, managing risk is essential to long-term market success. In a future post, we’ll look into techniques like position sizing, stop-loss strategies, R multiples, and portfolio optimization to help protect our capital and maximize returns. Then, we’ll round out this series by deploying our agent to the cloud.

So stay tuned, my fellow traders, engineers, and AI enthusiasts! In the meantime, keep honing your skills, stay disciplined, and never stop learning. The market rewards those who persist and adapt as we continue swapping symbols.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.