avatarAndreas

Summary

The provided content outlines a comprehensive tutorial on coding trading strategies in Python, specifically focusing on creating strategies using the Moving Average Convergence/Divergence (MACD) and the Awesome Oscillator (AO) with Ethereum price data from Kraken's API.

Abstract

The article "The A-Z of Coding a Trading Strategy. A Python Series" serves as an extensive guide for Python enthusiasts and at-home traders interested in developing and testing their own trading strategies. It covers the entire lifecycle of a trading strategy, from procuring market data to assessing strategy efficacy. The tutorial utilizes $ETH price data in USD from Kraken and demonstrates the construction of two simplified trading strategies based on technical indicators: the MACD and the AO. Emphasizing the importance of understanding indicators before implementation, the article guides readers through fetching historical data, calculating technical indicators, generating trading signals, and backtesting strategies. While the strategies presented are for educational purposes, the article underscores the necessity of considering additional factors such as trading costs, market influence, and risk management in practical applications. The use of real market data and Python libraries like ccxt, pandas, ta, and quantstats provides a hands-on approach to learning, with the ultimate goal of enhancing Python skills and not offering financial advice.

Opinions

  • The author emphasizes that the strategies discussed are simplified and intended for educational purposes, not as financial advice.
  • Real market data is used to enhance the learning experience and provide a more realistic simulation.
  • The importance of considering a comprehensive set of factors in a pragmatic trading strategy is highlighted, including trading costs, market influence, and risk management.
  • The article suggests that tangible trading conditions, such as existing portfolio holdings, can significantly influence the outcome of a strategy.
  • The author recommends using the quantstats Python library for detailed backtesting reports, indicating a preference for this tool in the analysis of trading strategy performance.
  • There is an acknowledgment that the historical data provided by Kraken's API (720 days) may not be sufficient for comprehensive backtesting, with a recommendation for at least 5–10 years of data to include various market conditions.
  • The tutorial advocates for the importance of understanding technical indicators before adding them to a trading strategy.
  • The author encourages readers to stay tuned for more articles in the Python series, indicating an intent to provide further educational content in the future.

The A-Z of Coding a Trading Strategy. A Python Series

A comprehensive tutorial to coding your first trading strategy.

DALL-E generated image

Introduction

Creating a personal trading strategy is becoming more and more popular amongst at-home traders and/or Python enthusiasts. This article will go through the complete lifecycle of a trading strategy: starting from procuring market data, computing technical indicators, generating trading signals and finally assessing the efficacy of the strategy.

We will use $ETH prices in USD from Kraken’s API and build two strategies based on the Moving Average Convergence/Divergence (MACD) and the Awesome Oscillator (AO).

By the end of this article, one should have a robust foundation to build on, and the capability to test its own trading strategies and assess their potential.

That said, it’s critical to note that the strategies we are deploying here are simplified versions designed for demonstrative purposes. There are additional factors that a pragmatic trading strategy ought to consider, such as trading costs, market influence, risk management, and more. Moreover, tangible trading conditions, like existing portfolio holdings, can significantly influence the outcome of a strategy. Although we’ll use real market data, keep in mind that this article is a tutorial, not financial advice, and aims at enhancing our Python skills.

With this in mind, let’s create our own trading strategy!

Part 1: Fetching historical data from Kraken

We first import our required libraries and then initialize our public API. While we are fetching data from Kraken, note that the “ccxt” Python library can be used to fetch data from multiple exchanges like Binance. Be aware that symbol and timeframe formats may vary between exchanges, consequently you might need to check “ccxt” documentation before fetching new data from other exchanges.

We define the symbol and the timeframe. We are looking for Ethereum “$ETH” traded in USD on Kraken “ETH/USD” and daily data.

“fetch_ohlcv” retrieves the OHLCV (Open, High, Low, Close, Volume) data for the symbol and timeframe specified: OHLCV data is fundamental for creating a strategy or conducting various types of technical analysis.

import ccxt
import pandas as pd

# Kraken API
exchange = ccxt.kraken()

# We define the symbol and timeframe
symbol = 'ETH/USD'
timeframe = '1d'

# We fetch OHLCV data
ohlcv = exchange.fetch_ohlcv(symbol, timeframe)

# We first convert to DataFrame then to Timestamp for a more readable format
df = pd.DataFrame(ohlcv, columns=['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume'])
df['Timestamp'] = pd.to_datetime(df['Timestamp'], unit='ms')
df.set_index('Timestamp', inplace=True)

# We now have our DataFrame
df
Our OHLCV data from Kraken

Here, let’s keep in mind that our DataFrame “df” has 720 entries. 1 entry represents 1 day. It’s important to determine the number of days of historical data available for technical indicators and backtesting purposes.

Part 2: Calculating the MACD and the AO

We now calculate the MACD and AO from our fetch data. The definitions below come from Investopedia and IG International. You will find the links in the ressources section.

“The Moving Average Convergence/Divergence (MACD) is a trend-following momentum indicator that shows the relationship between two exponential moving averages (EMAs) of a security’s price. The MACD line is calculated by subtracting the 26-period EMA from the 12-period EMA.

The result of that calculation is the MACD line. A nine-day EMA of the MACD line is called the signal line, which is then plotted on top of the MACD line, which can function as a trigger for buy or sell signals.” — Investopedia

For the MACD, we compute the MACD Line, the Signal Line, and the MACD Histogram in order to visualize it completely and develop a proper strategy.

“The Awesome Oscillator (AO) is a market momentum indicator which compares recent market movements to historic market movements. It uses a zero line in the centre, either side of which price movements are plotted according to a comparison of two different moving averages.” — IG International

For the AO, we compute a histogram anchored around a zero line.

We calculate both indicators using the “ta” Python library.

# Calculate MACD Line, Signal Line and MACD Histogram
macd = ta.trend.MACD(df['Close'])

df['macd_line'] = macd.macd()  # MACD Line
df['macd_signal'] = macd.macd_signal()  # Signal Line
df['macd_hist'] = macd.macd_diff()  # MACD Histogram (MACD Line - Signal Line)

# Calculate Awesome Oscillator
ao = ta.momentum.AwesomeOscillatorIndicator(df['High'], df['Low'])

df['ao'] = ao.awesome_oscillator()  # Awesome Oscillator

df
Our DataFrame “df” with the newly calculated indicators

Part 3: Dropping the NaN values from our DataFrame

This is a brief but crucial part of the tutorial. Given that the data for MACD and AO can’t be calculated due to the absence of historical prices before 2021–05–13, we drop all the NaN values from our DataFrame “df”. Always understand an indicator before adding it to your strategy!

# Drop NaN values
df.dropna(inplace=True)
df.info()

We now have prices for 687 days: from the 2021–06–15 to the 2023–05–02. Note that if you reproduce this code at home, you will fetch data that will have a different timeframe from this article (since Kraken only allows you to get data from the last 720 days, there will be a lag). But not to worry, it’s still the same process!

Part 4: Plotting the ETH prices and the indicators

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, ax = plt.subplots(3, figsize=(12,12), dpi=200)

# Plot ETH prices
ax[0].plot(df.index, df['Close'], label='ETH/USD')
ax[0].set_title('ETH/USD Prices - Last 687 days')
ax[0].legend()
ax[0].set_xlim([df.index.min(), df.index.max()])

# Plot MACD Line, Signal Line and Histogram
ax[1].plot(df.index, df['macd_line'], label='MACD Line') # Plot MACD Line and Signal Line
ax[1].plot(df.index, df['macd_signal'], label='Signal Line') # Plot MACD Line and Signal Line
ax[1].bar(df.index, df['macd_hist'], alpha=0.5, label='MACD Histogram') # Plot MACD Histogram as bar plot
ax[1].set_title('MACD')
ax[1].legend()
ax[1].set_xlim([df.index.min(), df.index.max()])

# Plot Awesome Oscillator
ax[2].bar(df.index, df['ao'], alpha=0.5, label='Awesome Oscillator', color='gray')
ax[2].set_title('Awesome Oscillator')
ax[2].legend()
ax[2].set_xlim([df.index.min(), df.index.max()])

# Set date format
date_format = mdates.DateFormatter('%b %y')
ax[0].xaxis.set_major_formatter(date_format)
ax[1].xaxis.set_major_formatter(date_format)
ax[2].xaxis.set_major_formatter(date_format)

# Show the plot
plt.tight_layout()
plt.show()
ETH/USD Prices with MACD and AO — Last 687 days

Part 5: Generating trading signals

We’ll define two strategies based on the MACD and AO signals.

Strategy 1: Buy when both MACD and AO are positive, sell when they’re both negative.

Strategy 2: Buy when MACD crosses above the signal line and AO crosses above zero, sell when MACD crosses below the signal line and AO crosses below zero.

import numpy as np

# Strategy 1
# Generating the buy "signal1"
df['signal1'] = np.where((df['macd_line'] > df['macd_signal']) & (df['ao'] > 0), 1, 0)
# Generating the "sell_signal1"
df['sell_signal1'] = np.where((df['macd_line'] < df['macd_signal']) & (df['ao'] < 0), 1, 0)

# Strategy 2
# Generating the buy "signal2" 
df['signal2'] = np.where((df['macd_line'] > df['macd_signal']) & (df['macd_line'].shift() < df['macd_signal'].shift()) & (df['ao'] > 0) & (df['ao'].shift() < 0), 1, 0)
# Generating the "sell_signal2"
df['sell_signal2'] = np.where((df['macd_line'] < df['macd_signal']) & (df['macd_line'].shift() > df['macd_signal'].shift()) & (df['ao'] < 0) & (df['ao'].shift() > 0), 1, 0)

# Counting the buy/sell signals
num_signals1 = df['signal1'].sum()
num_sells1 = df['sell_signal1'].sum()
num_signals2 = df['signal2'].sum()
num_sells2 = df['sell_signal2'].sum()

print(f"Number of buy signals in Strategy 1: {num_signals1}")
print(f"Number of sell signals in Strategy 1: {num_sells1}")
print(f"Number of buy signals in Strategy 2: {num_signals2}")
print(f"Number of sell signals in Strategy 2: {num_sells2}")

In summary, both strategies rely on the MACD Line, Signal Line, and AO value to generate buy and sell signals. Strategy 1 is based on simple intersections of the MACD Line and Signal Line, along with the AO value being above or below zero. Strategy 2, on the other hand, focuses on the crossing points of the MACD Line and Signal Line, as well as the crossing points of the AO value above and below zero.

Strategy 2 is more sensitive because it is looking for the points at which the MACD Line crosses the Signal Line and when the AO value crosses zero. These crossing points typically represent changes in market momentum, which is why the strategy is said to be sensitive to changes in momentum.

Part 6: Plotting the signals on the graph

fig, ax = plt.subplots(4, figsize=(12, 30))

# Plot ETH prices
ax[0].plot(df.index, df['Close'], label='ETH/USD')
ax[0].scatter(df[df['signal1'] == 1].index, df[df['signal1'] == 1]['Close'], color='g', marker='^', label='Buy Signal - Strategy 1')
ax[0].scatter(df[df['sell_signal1'] == 1].index, df[df['sell_signal1'] == 1]['Close'], color='r', marker='v', label='Sell Signal - Strategy 1')
ax[0].scatter(df[df['signal2'] == 1].index, df[df['signal2'] == 1]['Close'], color='b', marker='o', label='Buy Signal - Strategy 2', alpha=0.7)
ax[0].scatter(df[df['sell_signal2'] == 1].index, df[df['sell_signal2'] == 1]['Close'], color='orange', marker='x', label='Sell Signal - Strategy 2', alpha=0.7)
ax[0].set_title('ETH/USD Prices - Last 687 days')
ax[0].legend()
ax[0].set_xlim([df.index.min(), df.index.max()])

# Plot MACD Line, Signal Line and Histogram
ax[1].plot(df.index, df['macd_line'], label='MACD Line')
ax[1].plot(df.index, df['macd_signal'], label='Signal Line')
ax[1].bar(df.index, df['macd_hist'], alpha=0.5, label='MACD Histogram')
ax[1].set_title('MACD')
ax[1].legend()
ax[1].set_xlim([df.index.min(), df.index.max()])

# Plot Awesome Oscillator
ax[2].bar(df.index, df['ao'], alpha=0.5, label='Awesome Oscillator', color='gray')
ax[2].set_title('Awesome Oscillator')
ax[2].legend()
ax[2].set_xlim([df.index.min(), df.index.max()])

# Let's also set the appropriate date format
date_format = mdates.DateFormatter('%b %y')
for a in ax:
    a.xaxis.set_major_formatter(date_format)

# Plot
plt.tight_layout()
plt.show()
Hundreds of buy and sell signals for strat1, only 3 for strat2

Part 7: Backtesting

To evaluate our strategies, we calculate the daily returns based on the generated buy and sell signals: we calculate the daily returns as the percentage change in the closing price.

Then, we calculate the returns for each strategy by multiplying the daily returns with the respective signals (shifted by one day to avoid forward-looking bias). Cumulative returns are then calculated by cumulatively multiplying the daily returns (plus one); it simulates the compounding of returns as if you reinvested the proceeds of each trade.

Strategy 2 generated only 1 buy and 2 sell signals. We won’t be backtesting this strategy.

We recommend using the “quantstats” Python library for detailed reports.

import quantstats as qs

# We calculate the daily returns
df['returns'] = df['Close'].pct_change()

# Strategy 1 returns
df['strategy1_returns'] = df['returns'] * df['signal1'].shift()
df['strategy1_cum_returns'] = (1 + df['strategy1_returns']).cumprod()

# Drop the NaN values
df.dropna(inplace=True)

# Let's print the report for Strategy 1
print("Strategy 1:")
qs.reports.full(df['strategy1_returns'])
Report generated by Quantstats — Strat1
Report generated by Quantstats — Strat1

Quantstats gives us the EOY returns, the distribution of monthly returns, daily returns, the rolling volatility (6-months), the rolling sharpe (6-months), the rolling sortino (6-months), and so on.

Here, we only got 720 historical prices from Kraken’s API and are currently using 687 for backtesting. It’s enough for our tutorial but keep in mind that when conducting a backtest you need to have data that includes all market conditions (bull markets, bear markets, periods of high volatility, periods of low volatility); therefore sometimes much more than 687 days! Indeed, a commonly recommended minimum for many strategies would be at least 5–10 years.

Also, in more advanced backtesting, you would ideally simulate the strategy as closely as possible to how it would be implemented in real trading conditions. Like keeping track of whether you’re currently holding a position or not. It would also include transaction costs, slippage, and other practical considerations.

And it’s done. We hope you now have a robust foundation to build new strategies and assess their potential!

Thanks for reading and stay tuned for more articles in this Python series!

Ressources:

Brian Dolan, “MACD Indicator Explained, with Formula, Examples, and Limitations”, Investopedia, March 15, 2023.

IG International, “A trader’s guide to using the awesome oscillator”, IG International, Updated Monthly.

QuantStats Library, QuantStats Documentation, last checked on May 10, 2023.

Ta Library, Ta Documentation, last checked on April 28, 2023.

Python
Quantitative Strategies
Cryptocurrency
Quantitative Trading
Finance
Recommended from ReadMedium
avatarAakash Chavan Ravindranath, Ph.D
🚨 Why You Need an Alternative to yfinance

3 min read