Trading A-Z #4: Basic Trading Performance Evaluation Measures.
Article #4 on Trading Performance Evaluation Measures.
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 just published a new book after the success of my previous one “New Technical Indicators in Python”. It features a more complete description and addition of structured trading strategies with a GitHub page dedicated to the continuously updated code. If you feel that this interests you, feel free to visit the below link, or if you prefer to buy the PDF version, you could contact me on LinkedIn.
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
# Cleaning
Data = jump(Data, lookback)
return Data
def ema(Data, alpha, lookback, what, where):
alpha = alpha / (lookback + 1.0)
beta = 1 - alpha
# First value is a simple SMA
Data = ma(Data, lookback, what, where)
# Calculating first EMA
Data[lookback + 1, where] = (Data[lookback + 1, what] * alpha) + (Data[lookback, where] * beta)
# Calculating the rest of EMA
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, width = 1, genre = 'Smoothed'):
# Adding a few columns
Data = adder(Data, 5)
# Calculating Differences
for i in range(len(Data)):
Data[i, where] = Data[i, close] - Data[i - width, close]
# Calculating the Up and Down absolute values
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])
# Calculating the Smoothed Moving Average on Up and Down absolute values
if genre == 'Smoothed':
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)
if genre == 'Simple':
Data = ma(Data, lookback, where + 1, where + 3)
Data = ma(Data, lookback, where + 2, where + 4)
# Calculating the Relative Strength
Data[:, where + 5] = Data[:, where + 3] / Data[:, where + 4]
# Calculate the Relative Strength Index
Data[:, where + 6] = (100 - (100 / (1 + Data[:, where + 5])))
# Cleaning
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)
And now, the trading conditions (rules) we have mentioned above can be written in the below signal function.
lower_barrier = 20
upper_barrier = 80
def signal(Data, indicator_column, buy, sell):
Data = adder(Data, 10)
for i in range(len(Data)):
if Data[i, indicator_column] < lower_barrier and Data[i - 1, indicator_column] > lower_barrier and Data[i - 2, indicator_column] > lower_barrier :
Data[i, buy] = 1
if Data[i, indicator_column] > upper_barrier and Data[i - 1, indicator_column] < upper_barrier and Data[i - 2, indicator_column] < 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.
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
If you are also interested by more technical indicators and using Python to create strategies, then my best-selling book on Technical Indicators may interest you:
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
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
Conclusion
In this article, we have seen how easy it is to calculate the metrics. We can also add ratios such as the Sharpe and Calmar ratios. Risk metrics can also be added, for example, the maximum drawdown and the realized risk-reward ratio.