Introduction
Hey there, fellow traders and AI enthusiasts! I’m back with another installment in this series on building an AI-powered stock analysis tool. In my last post, I supercharged the agent with relative strength rankings, allowing it to identify the market’s leaders. But why stop there? Today, I’ll take the 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, one can generate insightful stock charts and have the AI agent, powered by Anthropic’s Claude 3 Opus, interpret them.
Here’s the updated repo.
Turning Charts into Insights
Before the AI agent can do its magic, I must convert the 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, one can easily add traces for any technical indicator they want, whether from the pandas_ta library or one they’ve created themselves.
Once I’ve generated these charts, I’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. I’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 the stock data into rich visual charts and encoding them in a format that Claude can process, I’m giving the AI agent the information it needs to perform deep, insightful technical analysis. It’s like giving the AI a pair of expert trader’s eyes to spot patterns and trends in the markets.
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. I’ll start by importing the necessary libraries and defining the input schema. If you’ve been following along, you know the pattern.
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, I’ll define a new tool for the agent, get_stock_chart_analysis
, using the LangChain @tool
decorator:
@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 the agent’s tool belt and updating its prompt with instructions on when to use it, the 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, the AI agent can now offer depth and nuance in its stock analysis. In the coming posts, I’ll explore even more features that will take AI-powered trading to the next level.
Get ready to dive into universe scanning, where I’ll teach the agent to identify the most promising opportunities across multiple stocks using the tools I’ve already defined. I’ll also delve (oh yes, delve) into the fascinating realm of pattern recognition, enabling the AI to spot classic chart patterns that every trader should know.
But perhaps most importantly, I’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 one’s risk and preserving one’s capital. I’ll explore how to integrate robust risk management techniques into the AI agent’s decision-making process, ensuring that it finds great trades and knows when to cut losses and protect one’s hard-earned gains. So, dust off that terminal, grab a pandas reference, and pull up a chair while I continue swapping symbols.
Leave a Reply