Published on

Evaluating a Trading Strategy Performance - Part 2

Authors

Hi!

In this post we'll pick up from the previous one and finish the last bit of this topic, which is about the evaluation of a trading strategy performance through the computation of some key metrics.

The tune for today:

Let's jump straight into our quantitative things.

The performance metrics

In the first part of this post, we saw how to calculate:

  • Buy and hold return and annualised return
  • Trading strategy return and annualised return
  • Sharpe ratio
  • Total Number of Trades
  • Hit ratio
  • Average profit per trade
  • Average loss per trade

We will conclude this subject with the calculation of:

  • Maximum drawdown
  • Day needed to recover from the maximum drawdown

And we will also plot the graph of the drawdown and the graph to compare the return of the buy and hold investment and the return of the trading strategy.

As mentioned in the first part, This code is from my project called "Trading Tesla with Machine Learning and Sentiment Analysis". You can find the full project code repository on GitHub. Buy and sell signals used for the trading strategy were produced as predictions by a Random Forest classifier fed with several technical indicators and sentiment scores on Twitter posts relevant to Tesla.

Let's go!

The packages required for the code in this and the previous post:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

As we explained previously, the drawdown is the downward movement of the strategy return from a peak to the lowest point before we are again into profit. As the name suggests, the maximum drawdown is the largest decrease (or loss) of funds in the account on a given period.

In the following algorithm, we work out both the maximum drawdown occurring in our account's balance and the number of days passed before recovering completely and going into profit again (or no recovery at all, if that happens).

# Max drawdown and days to recover from max drawdown

# Identify the maximum drawdown in our data and create a rounded version for the metrics summary
max_drawdown = df['Drawdown'].min()
max_drawdown_pct = round(df['Drawdown'].min(), 2)

# Set to zero the count of days we will increment after we incur a drawdown
df['Days_Drawdown_Recovery'] = 0

# Instantiate day counter, the count trigger for number of days in maximum drawdown
c = 1
trigger = False
days_max_drawdown_recovery = 0

# Loop through the drawdown time series
for index, row in df.iterrows():
    # Start counting drawdown days if return is negative
    if row['Drawdown'] < 0:
        df.at[index, 'Days_Drawdown_Recovery'] = c
        c += 1
    # Reset count and trigger if recovered from drawdown
    elif row['Drawdown'] >= 0:
        c = 1
        trigger = False
    # Catch the maximum drawdown
    if row['Drawdown'] == max_drawdown:
        trigger = True
        days_max_drawdown_recovery = df.loc[index, 'Days_Drawdown_Recovery']
        continue
    # Update days count if looping over maximum drawdown tail
    if trigger and (row['Drawdown'] < 0):
        days_max_drawdown_recovery = df.loc[index, 'Days_Drawdown_Recovery']
# If loop ends in drawdown, never recovered from drawdown
if trigger:
    days_max_drawdown_recovery = 'Never recovered'

Although it's a bit contorted and requires to loop in the dataset rows, the algorithm above does the job. If you find a more elegant way to retrieve the two values by only using Pandas filtering in a vectorised fashion, let me know, I'd be curious to see your solution.

We have now all the metrics we wanted. And because you should also please the eye but mainly because we love to be tidy and see the big picture at first glance, let's put all the performance metrics calculated in a neat summary table:

# Performance indicators summary table
# The "str()" function in the first value will cast all values to Python string data type
# So we are covered if "days_max_drawdown_recovery" results in 'Never recovered'
performance_summary_dict = {'Return Buy and Hold (%)' : str(returns_buy_hold),
                            'Return Buy and Hold Ann. (%)' : returns_buy_hold_ann,
                            'Return Trading Strategy (%)' : returns_strategy,
                            'Return Trading Strategy Ann. (%)' : returns_strategy_ann,
                            'Sharpe Ratio Ann.' : sharpe_ratio,
                            'Total Number of Trades' : n_trades,
                            'Hit Ratio (%)' : hit_ratio,
                            'Average Trades Profit (%)' : average_trades_profit,
                            'Average Trades Loss (%)' : average_trades_loss,
                            'Max Drawdown (%)' : max_drawdown_pct,
                            'Days Max Drawdown Recovery': days_max_drawdown_recovery}

performance_summary = pd.DataFrame.from_dict(performance_summary_dict, orient='index').rename(columns={0:'Performance Indic
display(performance_summary)

Plotting the drawdown and the returns

Finally, we use matplotlib.pyplot and the beautiful seaborn library to plot the graphs for the drawdown and the return in the two scenarios of buy and hold and trading strategy:

# Some plotting settings
text_size = 19
x_ticks_size = 14
y_ticks_size = x_ticks_size
pad_title = 15
pad_xlabel = 20
pad_ylabel = 70

# Plot drawdown
sns.set_style('darkgrid')
sns.set_palette(sns.color_palette("RdBu", 10))
fig, ax = plt.subplots()
fig.set_size_inches(11.7, 8.3)
sns.lineplot(x=df.index, y='Drawdown', data=df)
plt.xlabel('Date', labelpad=pad_xlabel, size=text_size)
plt.ylabel('Drawdown (%)', rotation=0, labelpad=pad_ylabel, size=text_size)
plt.xticks(fontsize=x_ticks_size)
plt.yticks(fontsize=y_ticks_size)
plt.title(f'Drawdown - Trading Strategy - {download_params["ticker"]}', size=text_size*1.7, pad=pad_title)

# Plot buy annd hold and strategy returns
sns.set_style('darkgrid')
sns.set_palette(sns.color_palette("Dark2"))
fig, ax = plt.subplots()
fig.set_size_inches(11.7, 8.3)
sns.lineplot(x=df.index, y='Capital_B&H', data=df)
sns.lineplot(x=df.index, y='Capital_Strategy', data=df)
plt.xlabel('Date', labelpad=pad_xlabel, size=text_size)
plt.ylabel('Equity', rotation=0, labelpad=pad_ylabel, size=text_size)
plt.xticks(fontsize=x_ticks_size)
plt.yticks(fontsize=y_ticks_size)
title = f'Equity Curve - Buy And Hold VS Trading Strategy - {download_params["ticker"]}\nInitial Capital: {initial_capital}'
plt.title(title, size=text_size*1.7, pad=pad_title)
ax.legend(labels=['Buy And Hold', 'Trading Strategy'], loc='upper left', fontsize=x_ticks_size, frameon=False)

Visualising the strategy performance results

When we run the last two blocks of code, we'll print the table and the plots shown below, and we have a more comprehensive view about the performance of our trading strategy.

Performance Indicators Summary
Return Buy and Hold (%)1618.59
Return Buy and Hold Ann. (%)305.94
Return Trading Strategy (%)1892.7
Return Trading Strategy Ann. (%)336.64
Sharpe Ratio Ann.17.47
Total Number of Trades244
Hit Ratio (%)90
Average Trades Profit (%)1.07
Average Trades Loss (%)-0.2
Max Drawdown (%)-1.24
Days Max Drawdown Recovery3

dd

ec

From the table and the plots we can infer some useful findings:

  • By trading the 20% of the capital at each trade and with a transaction fee of 0.05%, the trading strategy outperforms the return of the simple investment in the stock on the same period, with a return of 18 times the initial capital compared to the return of 1618% of the stock.
  • The hit ratio of 90% suggests that most of the trades were profitable, and along with the average trades profit of 1%, as opposed to the average loss of 0.2%, tells us that the trading strategy was overall made of successful trades and little losses
  • In the same regard, the maximum drawdown of 1.24% sounds quite safe in terms of risk on the capital, and it is worth to note that this occurs exactly during the market crash due to the beginning of the lockdowns around the world because of the Covid-19 pandemic. Here the trading strategy continued to perform very well, whereas we can see a significant crash on the stock price, which was tied to the plummeting of all the financial markets
  • It took three days to recover completely from the maximum drawdown, which again suggests that the trading strategy was making very well even under adverse market conditions. The machine learning algorithm seems to have caught insightful patterns and produced correct signals
  • The annualised Sharpe ratio of 17.47 sounds excellent. The volatility of the returns, as we saw in the Sharpe ratio formula, is inversely proportional to this indicator. In fact, when we look at the maximum drawdown and the average trade profit and loss, we can confirm that there weren't extreme values for the trade returns, and the daily returns on which the Sharpe Ratio is computed, were even smaller. This explains the high value of the indicator for our trading strategy.

And that's all for this post.

I hope you found this content useful and grasped the benefits deriving from using several performance metrics to analyse the ins and outs of a given strategy.

Thank you and see you in the next post!

Renato

Disclaimer

Please be aware that the content and results shown in this post do not represent financial advice. You should conduct your own research before trading or investing in the markets. Your capital is at risk.