Visualizing Trading Signals in Python

Plot buy and sell trading signals in Python’s graph

EODHD APIs
10 min readMay 13, 2024

We’re going to assume you are familiar with the basics of technical analysis in trading. If not, we suggest using a great resource like Investopedia to learn more.

If you’re looking to learn how to install the EODHD APIs Python Financial Official Library and activate your API key, start by exploring our comprehensive documentation.

Let’s consider a basic example:

We aim to identify the optimal time to buy or sell stocks or cryptocurrencies. How can we achieve this? One way is through moving averages to discern market direction.

A Simple Moving Average (SMA) is the average of the closing prices over the last ’n’ periods of historical trading data. For instance, in a daily chart, each candlestick or data point represents one day. The SMA50 provides the average of the last 50 days of closing prices, which helps smooth out the market “noise.” However, it becomes more useful when we compare it with another moving average, like the SMA200.

When the SMA50 crosses above the SMA200, it signals an upward market trend. Conversely, when the SMA50 crosses below the SMA200, it’s a sign of a downward trend. While any moving averages can be used, this particular combination is unique. When the SMA50 crosses above the SMA200, it’s referred to as the “Golden Cross”, and when it dips below, it’s called the “Death Cross”. These events often precede significant price movements.

What do we need to start visualizing trading signals in Python?

We’re using a Jupyter notebook in Google Colab, which is a free and convenient tool if you’d like to follow along. For demonstration purposes, we’re also utilizing the EODHD API’s end-of-day demo key.

Step 1: Import Necessary Libraries

To begin, import the following libraries:

import json
import requests
import pandas as pd
import matplotlib.pyplot as plt

Step 2: Retrieve Apple’s Daily Data

Next, retrieve Apple’s (AAPL) daily data using the API and store it in a Pandas DataFrame:

resp = requests.get("https://eodhistoricaldata.com/api/eod/AAPL?api_token=demo&fmt=json")
json_data = json.loads(resp.content)
df = pd.DataFrame(json_data)
df

If everything is working as expected, the resulting DataFrame should contain Apple’s daily stock data. This data will form the basis for analyzing and visualizing trading signals.

With 10,534 days of Apple’s historical data available, it’s worth noting that while “close” and “adjusted_close” values are quite similar, we’ll use “close” for this tutorial.

Now, let’s calculate and add the SMA50 and SMA200 columns to our DataFrame:

df["sma50"] = df.close.rolling(50, min_periods=50).mean()
df["sma200"] = df.close.rolling(200, min_periods=200).mean()
df

In the updated DataFrame, you will notice two new columns labeled “sma50” and “sma200.” Any “NaN” values appear because each SMA requires a minimum number of data points to calculate the first value. For instance, the SMA50 needs 50 closing prices before it can produce the initial average.

We have two ways to handle these “NaN” values: replace them with other values, such as the closing price, or simply remove the rows with missing data. Since we have a significant amount of data available, dropping the first 200 rows won’t affect our analysis.

Let’s drop the rows containing “NaN”:

df.dropna(inplace=True)
df

After removing the rows with missing values, our DataFrame will now contain only rows with fully populated SMA50 and SMA200 values.

What does this look like graphed?

In the next step, we’ll utilize the Matplotlib library, which is widely used in data science for visualizing data (as you may recall, we imported it earlier).

Here’s how to plot the trading signals:

plt.figure(figsize=(30,10))
plt.plot(df["close"], color="black", label="Price")
plt.plot(df["sma50"], color="blue", label="SMA50")
plt.plot(df["sma200"], color="green", label="SMA200")
plt.ylabel("Price")
plt.xticks(rotation=90)
plt.title("APPL Daily SMA50/SMA200")
plt.legend()
plt.show()

While the graph displays successfully, it’s not easy to interpret since it’s visualizing over 28 years of data, which is more than we need. To make it clearer, we can reduce the dataset to only the past year by limiting it to the last 365 days:

df = df.tail(365)

Now, this will show what the graph looks like with a more focused view.

You might notice that the x-axis currently shows index numbers instead of the actual dates. Although this isn’t necessarily problematic, it’s not particularly visually appealing. To improve this, we’ll replace the x-axis ticks with actual dates.

df.set_index(['date'], inplace=True)
df

And now, the updated graph will appear like this…

Currently, the x-axis displays all 365 days, resulting in a cluttered and hard-to-read graph. But don’t worry; we can fix this by adjusting the x-axis labels to show only every seventh day.

Here’s how the code works:

ax = plt.gca()
for index, label in enumerate(ax.xaxis.get_ticklabels()):
if index % 7 != 0:
label.set_visible(False)

In this code, we check if an index number is divisible by 7. If not, the corresponding x-axis label will be hidden, leaving only every seventh day visible. This makes the graph much clearer and easier to understand.

Here’s the complete code…

plt.figure(figsize=(30,10))
plt.plot(df["close"], color="black", label="Price")
plt.plot(df["sma50"], color="blue", label="SMA50")
plt.plot(df["sma200"], color="green", label="SMA200")
plt.ylabel("Price")
plt.xticks(rotation=90)
plt.title("APPL Daily SMA50/SMA200")
ax = plt.gca()
for index, label in enumerate(ax.xaxis.get_ticklabels()):
if index % 7 != 0:
label.set_visible(False)
plt.legend()
plt.show()

Excellent!

In the graph, you will notice a black line representing the daily closing price, a blue line for the SMA50, and a green line for the SMA200.

Observing the graph, the first time in the past year that the blue SMA50 line crosses above the green SMA200 line, the market enters an upward trend. This would have been an excellent and profitable time to buy. Later on, the blue line dips below the green line, which is followed by a significant price decline. Ideally, we wouldn’t want to wait too long before selling, and there are ways we could refine this.

How can we programmatically identify when the SMAs cross over?

In Pandas, we can create a new feature or column containing boolean values (true or false) based on a specific condition.

pd.options.mode.chained_assignment = None
df.loc[df["sma50"] > df["sma200"], "sma50gtsma200"] = True
df["sma50gtsma200"].fillna(False, inplace=True)
df.loc[df["sma50"] < df["sma200"], "sma50ltsma200"] = True
df["sma50ltsma200"].fillna(False, inplace=True)
df

In our DataFrame, you’ll notice two new columns: “sma50gtsma200” and “sma50ltsma200”. The first column returns `True` when the SMA50 is greater than the SMA200, while the second returns `True` when the SMA50 is less than the SMA200. If neither condition is met, the values will be `False`.

Now, we want to identify the precise point at which the crossover occurs. You can achieve this with the following approach:

df["sma50gtsma200co"] = df.sma50gtsma200.ne(df.sma50gtsma200.shift())
df.loc[df["sma50gtsma200"] == False, "sma50gtsma200co"] = False

df["sma50ltsma200co"] = df.sma50ltsma200.ne(df.sma50ltsma200.shift())
df.loc[df["sma50ltsma200"] == False, "sma50ltsma200co"] = False

df

The new feature columns, “sma50gtsma200co” and “sma50ltsma200co”, will indicate when crossovers occur.

Let’s confirm the buy signal(s):

buysignals = df[df["sma50gtsma200co"] == True]
buysignals

And the sell signal(s):

sellsignals = df[df["sma50ltsma200co"] == True]
sellsignals

Plotting Our Buy and Sell Trading Signals in Python

The next step involves plotting our buy and sell trading signals on the graph in Python. Here’s how to achieve this:

for idx in buysignals.index.tolist():
plt.plot(
idx,
df.loc[idx]["close"],
"g*",
markersize=25
)

for idx in sellsignals.index.tolist():
plt.plot(
idx,
df.loc[idx]["close"],
"r*",
markersize=25
)

This code will add green stars for entries in the “buysignals” DataFrame and red stars for entries in the “sellsignals” DataFrame. It will visualize all the buy and sell signals in the dataframes so that combining signals yields more advanced results.

The complete code looks like this…

plt.figure(figsize=(30,10))
plt.plot(df["close"], color="black", label="Price")
plt.plot(df["sma50"], color="blue", label="SMA50")
plt.plot(df["sma200"], color="green", label="SMA200")
plt.ylabel("Price")
plt.xticks(rotation=90)
plt.title("APPL Daily SMA50/SMA200")
for idx in buysignals.index.tolist():
plt.plot(
idx,
df.loc[idx]["close"],
"g*",
markersize=25
)

for idx in sellsignals.index.tolist():
plt.plot(
idx,
df.loc[idx]["close"],
"r*",
markersize=25
)

ax = plt.gca()
for index, label in enumerate(ax.xaxis.get_ticklabels()):
if index % 7 != 0:
label.set_visible(False)
plt.legend()
plt.show()

The benefit of plotting the buy and sell trading signals in Python is that it provides a clear visual representation of market movements, making it easier to refine trading strategies.

For instance, while the initial buy signal might look promising, the subsequent sell signal is delayed because of the inherent lag in moving averages.

Let’s incorporate the Moving Average Convergence Divergence (MACD) indicator and see if it can improve our signals.

df["ema12"] = df["close"].ewm(span=12, adjust=False).mean()
df["ema26"] = df["close"].ewm(span=26, adjust=False).mean()

df["macd"] = df["ema12"] - df["ema26"]
df["signal"] = df["macd"].ewm(span=9, adjust=False).mean()

df.loc[df["macd"] > df["signal"], "macdgtsignal"] = True
df["macdgtsignal"].fillna(False, inplace=True)

df.loc[df["macd"] < df["signal"], "macdltsignal"] = True
df["macdltsignal"].fillna(False, inplace=True)

df

To understand the MACD technical indicator, you can refer to Investopedia for more information. In brief, MACD is calculated by subtracting the 26-period Exponential Moving Average (EMA) from the 12-period EMA. The Signal is another Exponential Moving Average using the most recent 9 MACD values.

A buy signal occurs when the MACD crosses above the Signal, while a sell signal is the opposite. We identify these signals by adding feature columns to our DataFrame that capture the exact crossover points.

To combine both SMA50/200 and MACD/Signal indicators into composite buy and sell trading signals:

buysignals = df[(df["sma50gtsma200co"] == 1) & (df["macdgtsignal"] == 1)]

sellsignals = df[(df["sma50ltsma200co"] == 1) & (df["macdltsignal"] == 1)]

This logic dictates that a buy signal is triggered when the SMA50 crosses above the SMA200 and the MACD is greater than the Signal. Conversely, a sell signal is triggered when the SMA50 crosses below the SMA200 and the MACD is less than the Signal.

You’ll notice that the combined strategy still identifies our buy signal as accurate, but the two sell signals have been removed. In this case, it might suggest holding the position rather than selling. However, it’s uncertain if this is the right approach in this specific scenario. Further analysis may be needed to refine the signals.

If we modify our sell signal to indicate that, even when the SMA50 remains above the SMA200, a sell should occur when the MACD crosses below the Signal, we can adjust our conditions like this:

df["macdgtsignalco"] = df.macdgtsignal.ne(df.macdgtsignal.shift())
df.loc[df["macdgtsignal"] == False, "macdgtsignalco"] = False

df["macdltsignalco"] = df.macdltsignal.ne(df.macdltsignal.shift())
df.loc[df["macdltsignal"] == False, "macdltsignalco"] = False

buysignals = df[(df["sma50gtsma200co"] == 1) & (df["macdgtsignal"] == 1)]

sellsignals = df[(df["sma50gtsma200"] == 1) & (df["macdltsignalco"] == 1)]

With this adjusted condition, we now have five sell signals within our DataFrame, helping us refine our trading strategy by considering both moving average and MACD crossovers.

This setup now looks much better. The buy signal appears to be on point, and we have several sell signals available to choose from. Selling on that initial sell signal after buying, with leverage, would likely yield a decent profit.

This example provides a good understanding of the possibilities and the tools needed to bring them to life. Experiment with various technical indicators and candlestick patterns to create your own “magic strategy”.

Why not try adding the Relative Strength Index (RSI)? A buy signal is typically indicated if the RSI14 is below 30, and a sell signal is identified if the RSI14 is above 70.

To help you get started, remember that the calculation for RSI is a bit more complex than moving averages or MACD. Using the `pandas_ta` library simplifies this process.

In Google Colab, install the `pandas_ta` library using a separate code cell:

pip install pandas_ta

Then, import the library:

import pandas_ta as ta

Finally, calculate the RSI14:

df["rsi14"] = ta.rsi(df["close"], length=14, fillna=50)

Keep in mind that the first 14 entries of RSI14 will contain “NaN” values. By using `fillna`, we set the default value to 50.

We hope you found this article both interesting and useful!

Please note that this article is for informational purposes only and should not be taken as financial advice. We do not bear responsibility for any trading decisions made based on the content of this article. Readers are advised to conduct their own research or consult with a qualified financial professional before making any investment decisions.

For those eager to delve deeper into such insightful articles and broaden their understanding of different strategies in financial markets, we invite you to follow our account and subscribe for email notifications.

Stay tuned for more valuable articles that aim to enhance your data science skills and market analysis capabilities.

--

--

EODHD APIs

eodhd.com — stock market fundamental and historical prices API for stocks, ETFs, mutual funds and bonds all over the world.