Coding & Evaluating Trading Performance Metrics
Evaluating Trading Strategies Using Performance Metrics in Python
Being able to quickly evaluate your trading strategies can be a gold mine with regards to time saving. There are a lot of libraries that offer these functions but it is always better to do them yourself. In this article, we will create a trading strategy from scratch in Python and then evaluate it using our functions.
I have released a new book called “Contrarian Trading Strategies in Python”. It features a lot of advanced contrarian indicators and strategies with a GitHub page dedicated to the continuously updated code. If you are interested, you could buy the PDF version directly through a PayPal payment of 9.99 EUR.
Please include your email in the note before paying so that you receive it on the right address. Also, once you receive it, make sure to download it through google drive.
Pay Kaabar using PayPal.Me
If you accept cookies, we’ll use them to improve and customize your experience and enable our partners to show you…www.paypal.com
Creating a Simple Strategy
The first thing to create and back-test a strategy is the idea generation process. This can be the choice between mean-reversion and trend-following. Then, we need to choose our indicators or our method. For example, the barriers strategy on the relative strength index. Finally, we need to download historical data, apply the RSI and signal function, and then apply the performance functions that we will see below in the article.
Let assume that we want to back-test a simple strategy based on the 5-period RSI where we buy when it reaches 20 and go short (sell) when it reaches 80. We will also suppose that no risk management system will be used. The entry and exit rules will therefore be:
Go long (Buy) whenever the RSI reaches or breaks 20 with the two previous value above 20. Hold this position until getting a new signal where it is exited and the new position is initiated.
Go short (Sell) whenever the RSI reaches or surpasses 80 with the two previous value below 80. Hold this position until getting a new signal where it is exited and the new position is initiated.
Suppose we have an array (not a data frame) called my_data which holds OHLC data for an asset. In our case, it will be EURUSD. Therefore, the remainder of the article will do the research on hourly values of the EURUSD since January 2010.
# Importing the required libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Importing our OHLC data from an excel file
my_data = pd.read_excel('my_data.xlsx')
# Transforming into a numpy array
my_data = np.array(my_data)
The next step is to code the Relative Strength Index on the OHLC data we have just imported. This can be done using the below code:
def adder(data, times):
for i in range(1, times + 1):
new = np.zeros((len(data), 1), dtype = float)
data = np.append(data, new, axis = 1)
return data
def deleter(data, index, times):
for i in range(1, times + 1):
data = np.delete(data, index, axis = 1)
return data
def jump(data, jump):
data = data[jump:, ]
return data
def ma(data, lookback, close, where):
data = adder(data, 1)
for i in range(len(data)):
try:
data[i, where] = (data[i - lookback + 1:i + 1, close].mean())
except IndexError:
pass
data = jump(data, lookback)
return data
def ema(data, alpha, lookback, what, where):
alpha = alpha / (lookback + 1.0)
beta = 1 - alpha
data = ma(data, lookback, what, where)
data[lookback + 1, where] = (data[lookback + 1, what] * alpha) + (data[lookback, where] * beta)
for i in range(lookback + 2, len(data)):
try:
data[i, where] = (data[i, what] * alpha) + (data[i - 1, where] * beta)
except IndexError:
pass
return data
def rsi(data, lookback, close, where):
data = adder(data, 5)
for i in range(len(data)):
data[i, where] = data[i, close] - data[i - 1, close]
for i in range(len(data)):
if data[i, where] > 0:
data[i, where + 1] = data[i, where]
elif data[i, where] < 0:
data[i, where + 2] = abs(data[i, where])
lookback = (lookback * 2) - 1 # From exponential to smoothed
data = ema(data, 2, lookback, where + 1, where + 3)
data = ema(data, 2, lookback, where + 2, where + 4)
data[:, where + 5] = data[:, where + 3] / data[:, where + 4]
data[:, where + 6] = (100 - (100 / (1 + data[:, where + 5])))
data = deleter(data, where, 6)
data = jump(data, lookback)
return data
Now, to use it, we write this simple line below:
# Calculating the 5-period RSI
my_data = rsi(my_data, 5, 3, 4)
This should cause a new column to appear with the RSI values inside. The next step is to add a few columns that we will populate with the buy/sell orders and the profit/loss results.
def signal(Data, what, buy, sell):
for i in range(len(Data)):
if Data[i, what] < lower_barrier and Data[i - 1, what] > lower_barrier and Data[i - 2, what] > lower_barrier :
Data[i, buy] = 1
if Data[i, what] > upper_barrier and Data[i - 1, what] < upper_barrier and Data[i - 2, what] < upper_barrier :
Data[i, sell] = -1
Remember, the signal function is the one that inputs 1 (Buy) in the specified column when the buying conditions are met and inputs -1 (Sell) in the specified column when the selling conditions are met. Now, how do we use the previous code to initiate buy and sell signals on the OHLC with the RSI column data we have built?
# Adding a few extra empty columns
my_data = adder(my_data, 10)
# Using the signal function to get signals from the RSI at column 4 (fifth column) and input buy signals at column 6 (seventh column) and sell signals at column 7 (eighth column)
signal(my_data, 4, 6, 7)
We are all set now. We have our signals generated from the 5-period RSI as well as a few extra columns where we should put our results. The next part starts building the functions needed to evaluate the strategy.
Check out my weekly market sentiment report to understand the current positioning and to estimate the future direction of several major markets through complex and simple models working side by side. Find out more about the report through this link:
Coalescence Report 1st May — 8th May 2022
This report covers the weekly market sentiment and positioning and any changes that have occurred which might present…coalescence.substack.com
The Equity Curve
The quickest and probably the preferred visual performance interpretation is the Equity Curve. It is simply the evolution of the balance which invests in the strategy. In other words, it is the cumulative profits and losses when added to the initial investment.
The first step into building the Equity Curve is to calculate the profits and losses from the individual trades we are taking. For simplicity reasons, we can consider buying and selling closing prices. This means that when we get the signal from the RSI on close, we initiate the trade on the close until getting another signal where we exit and initiate the new trade. The code to be defined for the profit/loss columns is the below:
def holding(Data, buy, sell, buy_return, sell_return):for i in range(len(Data)):
try:
if Data[i, buy] == 1:
for a in range(i + 1, i + 1000):
if Data[a, buy] != 0 or Data[a, sell] != 0:
Data[a, buy_return] = (Data[a, 3] - Data[i, 3])
break
else:
continue
elif Data[i, sell] == -1:
for a in range(i + 1, i + 1000):
if Data[a, buy] != 0 or Data[a, sell] != 0:
Data[a, sell_return] = (Data[i, 3] - Data[a, 3])
break
else:
continue
except IndexError:
pass
# Using the function
holding(my_data, 6, 7, 8, 9)
This will give us columns 8 and 9 populated with the gross profit and loss results from the trades taken. Now, we have to transform them into cumulative numbers so that we calculate the Equity Curve. To do that, we use the below indexer function:
def indexer(Data, expected_cost, lot, investment):
# Charting portfolio evolution
indexer = Data[:, 8:10]
# Creating a combined array for long and short returns
z = np.zeros((len(Data), 1), dtype = float)
indexer = np.append(indexer, z, axis = 1)
# Combining Returns
for i in range(len(indexer)):
try:
if indexer[i, 0] != 0:
indexer[i, 2] = indexer[i, 0] - (expected_cost / lot)
if indexer[i, 1] != 0:
indexer[i, 2] = indexer[i, 1] - (expected_cost / lot)
except IndexError:
pass
# Switching to monetary values
indexer[:, 2] = indexer[:, 2] * lot
# Creating a portfolio balance array
indexer = np.append(indexer, z, axis = 1)
indexer[:, 3] = investment
# Adding returns to the balance
for i in range(len(indexer)):
indexer[i, 3] = indexer[i - 1, 3] + (indexer[i, 2])
indexer = np.array(indexer)
return np.array(indexer)
# Using the function for a 0.1 lot strategy on $10,000 investment
expected_cost = 0.5 * (lot / 10000) # 0.5 pip spread
investment = 10000
lot = 10000
equity_curve = indexer(my_data, expected_cost, lot, investment)
The above shows the Equity Curve on the EURUSD strategy we have been following. The below code is used to generate the chart. Note that the indexer function nets the returns using the estimated transaction cost, hence, the equity curve above is theoretically net of fees.
plt.plot(equity_curve[:, 3], linewidth = 1, label = 'EURUSD)
plt.grid()
plt.legend()
plt.axhline(y = investment, color = 'black', linewidth = 1)
plt.title('RSI Strategy', fontsize = 20)
The Hit Ratio
We will present quickly the main ratios and metrics before presenting a full performance function that outputs them all together. Hence, the below discussions are mainly informational, if you are interested by the code, you can find it at the end.
Hit ratio = 42.28 %
The hit ratio is extremely easy to use. It is simply the number of winning trades over the number of the trades taken in total. For example, if we have 1359 trades over the course of 5 years and we have been profitable in 711 of them , then our hit ratio (accuracy) is 711/1359 = 52.31%.
The Net Profit
The net profit is simply the last value in the Equity Curve net of fees minus the initial balance. It is simply the added value on the amount we have invested in the beginning.
Net profit = $ 1209.4
Average Gain & Average Loss
A quick glance on the average gain across the trades and the average loss can help us manage our risks better. For example, if our average gain is $1.20 and our average loss is $4.02, then we know that something is not right as we are risking way too much money for way too little gain.
Average Gain = $ 56.95 per trade
Average Loss = $ -41.14 per trade
Profit Factor
This is a relatively quick and straightforward method to compute the profitability of the strategy. It is calculated as the total gross profit over the total gross loss in absolute values, hence, the interpretation of the profit factor (also referred to as profitability index in the jargon of corporate finance) is how much profit is generated per $1 of loss. The formula for the profit factor is:
Profit factor = 1.01
Expectancy is a flexible measure that is composed of the average win/loss and the hit ratio. It provides the expected profit or loss on a dollar figure weighted by the hit ratio. The win rate is what we refer to as the hit ratio in the below formula, and through that, the loss ratio is 1 — hit ratio.
Expectancy = $ 0.33 per trade
Number of Trades
This is simply to understand the frequency of the trades we have.
Trades = 3697
Putting It All Together
Now, we are ready to have all of the above metrics shown at the same time. After calculating the indexer function, we can use the below performance function to give us the metrics we need:
def performance(indexer, Data, name):
# Profitability index
indexer = np.delete(indexer, 0, axis = 1)
indexer = np.delete(indexer, 0, axis = 1)
profits = []
losses = []
np.count_nonzero(Data[:, 7])
np.count_nonzero(Data[:, 8])
for i in range(len(indexer)):
if indexer[i, 0] > 0:
value = indexer[i, 0]
profits = np.append(profits, value)
if indexer[i, 0] < 0:
value = indexer[i, 0]
losses = np.append(losses, value)
# Hit ratio calculation
hit_ratio = round((len(profits) / (len(profits) + len(losses))) * 100, 2)
realized_risk_reward = round(abs(profits.mean() / losses.mean()), 2)
# Expected and total profits / losses
expected_profits = np.mean(profits)
expected_losses = np.abs(np.mean(losses))
total_profits = round(np.sum(profits), 3)
total_losses = round(np.abs(np.sum(losses)), 3)
# Expectancy
expectancy = round((expected_profits * (hit_ratio / 100)) \
- (expected_losses * (1 - (hit_ratio / 100))), 2)
# Largest Win and Largest Loss
largest_win = round(max(profits), 2)
largest_loss = round(min(losses), 2)
# Total Return
indexer = Data[:, 10:12]
# Creating a combined array for long and short returns
z = np.zeros((len(Data), 1), dtype = float)
indexer = np.append(indexer, z, axis = 1)
# Combining Returns
for i in range(len(indexer)):
try:
if indexer[i, 0] != 0:
indexer[i, 2] = indexer[i, 0] - (expected_cost / lot)
if indexer[i, 1] != 0:
indexer[i, 2] = indexer[i, 1] - (expected_cost / lot)
except IndexError:
pass
# Switching to monetary values
indexer[:, 2] = indexer[:, 2] * lot
# Creating a portfolio balance array
indexer = np.append(indexer, z, axis = 1)
indexer[:, 3] = investment
# Adding returns to the balance
for i in range(len(indexer)):
indexer[i, 3] = indexer[i - 1, 3] + (indexer[i, 2])
indexer = np.array(indexer)
total_return = (indexer[-1, 3] / indexer[0, 3]) - 1
total_return = total_return * 100
print('-----------Performance-----------', name)
print('Hit ratio = ', hit_ratio, '%')
print('Net profit = ', '$', round(indexer[-1, 3] - indexer[0, 3], 2))
print('Expectancy = ', '$', expectancy, 'per trade')
print('Profit factor = ' , round(total_profits / total_losses, 2))
print('Total Return = ', round(total_return, 2), '%')
print('')
print('Average Gain = ', '$', round((expected_profits), 2), 'per trade')print('Average Loss = ', '$', round((expected_losses * -1), 2), 'per trade')print('Largest Gain = ', '$', largest_win)print('Largest Loss = ', '$', largest_loss)
print('')
print('Realized RR = ', realized_risk_reward)
print('Minimum =', '$', round(min(indexer[:, 3]), 2))
print('Maximum =', '$', round(max(indexer[:, 3]), 2))
print('Trades =', len(profits) + len(losses))
# Using the function
performance(equity_curve, my_data, 'EURUSD)
This should give us something like the below:
-----------Performance----------- EURUSD
Hit ratio = 42.28 %
Net profit = $ 1209.4
Expectancy = $ 0.33 per trade
Profit factor = 1.01
Total Return = 120.94 %
Average Gain = $ 56.95 per trade
Average Loss = $ -41.14 per trade
Largest Gain = $ 347.5
Largest Loss = $ -311.6
Realized RR = 1.38
Minimum = $ -1957.6
Maximum = $ 4004.2
Trades = 3697
Summary
To sum up, what I am trying to do is to simply contribute to the world of objective technical analysis which is promoting more transparent techniques and strategies that need to be back-tested before being implemented. This way, technical analysis will get rid of the bad reputation of being subjective and scientifically unfounded.
I recommend you always follow the the below steps whenever you come across a trading technique or strategy:
Have a critical mindset and get rid of any emotions.
Back-test it using real life simulation and conditions.
If you find potential, try optimizing it and running a forward test.
Always include transaction costs and any slippage simulation in your tests.
Always include risk management and position sizing in your tests.
Finally, even after making sure of the above, stay careful and monitor the strategy because market dynamics may shift and make the strategy unprofitable.
For the paperback link of the book, you may use the following link:
Contrarian Trading Strategies in Python
Amazon.com: Contrarian Trading Strategies in Python: 9798434008075: Kaabar, Sofien: Bookswww.amazon.co