Seeing the Bigger Picture: Adding Chart-Based Technical Analysis

Introduction

Hey there, fellow traders and AI enthusiasts! We’re back with another installment in our series on building an AI-powered stock analysis tool. In my last post, I supercharged our agent with relative strength rankings, allowing it to identify the market’s leaders. But why stop there? Today, I’ll take our analysis to the next level by integrating chart-based technical analysis.

As any seasoned trader knows, a picture is worth a thousand words โ€“ or in this case, a thousand data points. By leveraging the power of Python libraries like Plotly and Imgur, we can generate insightful stock charts and have our AI agent, powered by Anthropic’s Claude 3 Opus, interpret them.

Here’s the updated repo.

Turning Charts into Insights

Before our AI agent can work magic, we must convert our stock data into a visual format that Claude 3 Opus can analyze. That’s where Plotly comes in โ€“ a powerful Python library that allows us to create highly customizable stock charts. With Plotly, we can easily add traces for any technical indicator we want, whether from the pandas_ta library or one we’ve created ourselves.

Once we’ve generated these charts, we’ll convert them into a base64 representation. This essentially encodes the image data into a string format that can be easily transmitted and stored. Of course, you can use S3 or another file store to persist these images for later access. We’ll then pass this base64-encoded image to Claude 3 Opus, designed to handle multimodal inputs (in this case, both text and images).

By converting our stock data into rich visual charts and encoding them in a format that Claude can process, we’re giving our AI agent the information it needs to perform deep, insightful technical analysis. It’s like giving our AI a pair of expert trader’s eyes to spot patterns and trends in the markets.

Python
import io
import os
import base64
import tempfile
from datetime import datetime, timedelta

import pandas as pd
import plotly.graph_objects as go
import plotly.subplots as sp
import pyimgur
import seaborn as sns

from dotenv import load_dotenv
from openbb import obb

from app.features.technical import add_technicals

load_dotenv()

IMGUR_CLIENT_ID = os.environ.get("IMGUR_CLIENT_ID")
IMGUR_CLIENT_SECRET = os.environ.get("IMGUR_CLIENT_SECRET")

sns.set_style("whitegrid")


def create_plotly_chart(df: pd.DataFrame, symbol: str) -> go.Figure:
    """
    Generate a Plotly chart for stock data visualization.

    This function creates a Plotly chart for a given DataFrame and stock symbol.
    It includes a candlestick chart with moving average overlays and subplots for RSI and ATR.

    Parameters:
    - df (pd.DataFrame): The DataFrame containing stock data with columns like 'open', 'high', 'low', 'close', 'SMA_50', 'SMA_200', 'RSI', 'ATR'.
    - symbol (str): The stock symbol.

    Returns:
    - go.Figure: A Plotly figure object that can be used to display the chart.
    """
    fig = sp.make_subplots(
        rows=3,
        cols=1,
        shared_xaxes=True,
        vertical_spacing=0.02,
        subplot_titles=("", "", ""),
        row_heights=[0.6, 0.2, 0.2],
    )

    # Candlestick chart
    fig.add_trace(
        go.Candlestick(
            x=df.index,
            name="Price",
            open=df["open"],
            high=df["high"],
            low=df["low"],
            close=df["close"],
        ),
        row=1,
        col=1,
    )

    # SMA_50 trace
    fig.add_trace(
        go.Scatter(
            x=df.index, y=df["SMA_50"], mode="lines", name="50-day SMA", line=dict(color="blue")
        ),
        row=1,
        col=1,
    )
    # SMA_200 trace
    fig.add_trace(
        go.Scatter(
            x=df.index, y=df["SMA_200"], mode="lines", name="200-day SMA", line=dict(color="red")
        ),
        row=1,
        col=1,
    )

    # RSI trace
    fig.add_trace(
        go.Scatter(
            x=df.index, y=df["RSI"], mode="lines", name="RSI", line=dict(color="orange")
        ),
        row=2,
        col=1,
    )
    fig.add_hline(y=70, line_dash="dash", line_color="red", row=2, col=1)
    fig.add_hline(y=30, line_dash="dash", line_color="green", row=2, col=1)

    # ATR trace
    fig.add_trace(
        go.Scatter(
            x=df.index, y=df["ATR"], mode="lines", name="ATR", line=dict(color="orange")
        ),
        row=3,
        col=1,
    )

    now = datetime.now().strftime("%m/%d/%Y")
    fig.update_layout(
        height=600,
        width=800,
        title_text=f"{symbol} | {now}",
        title_y=0.98,
        plot_bgcolor="lightgray",
        xaxis_rangebreaks=[
            dict(bounds=["sat", "mon"], pattern="day of week"),
        ],
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=1.02,
            xanchor="right",
            x=1
        ),
        font=dict(size=10)
    )

    fig.update_xaxes(rangeslider_visible=False)
    fig.update_yaxes(title_text="Price", row=1, col=1)
    fig.update_yaxes(title_text="RSI", row=2, col=1)
    fig.update_yaxes(title_text="ATR", row=3, col=1)

    return fig


def upload_image_to_imgur(buffer, symbol) -> str:
    """
    Uploads an image to Imgur.

    This function takes a buffer containing an image, uploads it to Imgur, and returns the URL of the uploaded image.
    It uses the Imgur API credentials defined in the environment variables.

    Args:
        buffer (io.BytesIO): The buffer containing the image data.
        symbol (str): The stock symbol associated with the image, used for titling the image on Imgur.

    Returns:
        str: The URL of the uploaded image on Imgur.
    """
    im = pyimgur.Imgur(
        IMGUR_CLIENT_ID, client_secret=IMGUR_CLIENT_SECRET, refresh_token=True
    )

    with tempfile.NamedTemporaryFile(suffix=".png") as tmp:
        tmp.write(buffer.getvalue())
        temp_path = tmp.name
        now = datetime.now().strftime("%m/%d/%Y")
        uploaded_image = im.upload_image(
            temp_path, title=f"{symbol} chart for {now}"
        )
        return uploaded_image.link


def plotly_fig_to_bytes(fig, filename="temp_plot.png"):
    """
    Convert a Plotly figure to a bytes object.

    This function takes a Plotly figure, saves it as a PNG image, reads the image back into memory,
    and then deletes the image file. It returns a bytes object of the image which can be used for further processing or uploading.

    Args:
        fig (plotly.graph_objs._figure.Figure): The Plotly figure to convert.
        filename (str): The filename to use when saving the image. Defaults to 'temp_plot.png'.

    Returns:
        io.BytesIO: A bytes object containing the image data.
    """
    fig.write_image(filename)
    with open(filename, "rb") as file:
        img_bytes = io.BytesIO(file.read())
    os.remove(filename)
    return img_bytes


def get_chart_base64(symbol: str) -> dict:
    """
    Generate a base64 encoded string of the chart image for a given stock symbol.
    Returns the base64 string and the figure object.

    Args:
    symbol (str): The stock symbol to generate the chart for.

    Returns:
    dict: A dictionary containing the base64 encoded string and the URL of the uploaded image on Imgur.
    """
    try:
        start = datetime.now() - timedelta(days=365 * 2)
        start_date = start.strftime("%Y-%m-%d")
        df = obb.equity.price.historical(
            symbol, start_date=start_date, provider="yfinance"
        ).to_df()

        if df.empty:
            return {"error": "Stock data not found"}

        df = add_technicals(df)
        chart_data = create_plotly_chart(df, symbol)
        chart_bytes = plotly_fig_to_bytes(chart_data)
        chart_url = upload_image_to_imgur(chart_bytes, symbol)

        chart_base64 = base64.b64encode(chart_bytes.getvalue()).decode('utf-8')
        return {"chart": chart_base64, "url": chart_url}
    except Exception as e:
        return {"error": f"Failed to generate chart: {str(e)}"}

Two Eyes, as Often as I Can Spare Them

Let’s explore the LangChain bits that make this possible. We’ll start by importing the necessary libraries and defining our input schema. If you’ve been following along, you know the pattern.

Python
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
from langchain.agents import tool

from app.features.chart import get_chart_base64

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

Next, we define a new tool for our agent, get_stock_chart_analysis, using the LangChain @tool decorator:

Python
@tool(args_schema=StockStatsInput)
def get_stock_chart_analysis(symbol: str) -> str:
    """Using the chart data, generate a technical analysis summary."""
    try:
        chart_data = get_chart_base64(symbol)
        llm = ChatAnthropic(model_name="claude-3-opus-20240229", max_tokens=4096)
        
        analysis = llm.invoke(
            [
                HumanMessage(
                    content=[
                        {
                            "type": "text",
                            "text": "Analyze the following stock chart image and provide a technical analysis summary:"
                        },
                        {
                            "type": "image_url",
                            "image_url": {"url": f"data:image/png;base64,{chart_data['chart']}"},
                        },
                    ]
                )
            ]
        )
        return f"\n<observation>\n{analysis}\n</observation>\n"
    except Exception as e:
        return f"\n<observation>\nError: {e}\n</observation>\n"

By integrating this new tool into our agent’s tool belt and updating its prompt with instructions on when to use it, our AI stock assistant can “see” the markets and incorporate that visual information into its analysis.

Conclusion

By harnessing the power of visual data and the advanced interpretive capabilities of Claude 3 Opus, our AI agent can now offer depth and nuance in its stock analysis. In the coming posts, we’ll explore even more features that will take our AI-powered trading to the next level.

Get ready to dive into universe scanning, where we’ll teach our agent to identify the most promising opportunities across multiple stocks using the tools we’ve already defined. We’ll also delve (oh yes, delve) into the fascinating realm of pattern recognition, enabling our AI to spot classic chart patterns that every trader should know.

But perhaps most importantly, we’ll tackle the crucial topic of risk management. After all, as any experienced trader will tell you, it’s not just about picking winners โ€“ it’s about managing your risk and preserving your capital. We’ll explore how to integrate robust risk management techniques into our AI agent’s decision-making process, ensuring that it finds great trades and knows when to cut losses and protect our hard-earned gains. So, dust off your terminal, grab a pandas reference and pull up a chair while 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.