1. Import libraries and set plotting style¶

This section is for loading necessary packages such pandas, numpy, matplotlib, and setting a consistent plotting style.

In [224]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt


# We loaded necessary packages.


# we use seaborn style for default plotting settings
plt.style.use('seaborn-v0_8')


pd.set_option('display.max_columns', 20)
pd.set_option('display.width', 120)

# the following is for making plots a little larger and consistent.
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['axes.titlesize'] = 13
plt.rcParams['axes.labelsize'] = 11
plt.rcParams['legend.fontsize'] = 10

2. Load stk1999.pkl and inspect the data¶

This section is for checking the data file, making sure that MSFT and SPY are there. Also confirming the data format so we know that what we are delaing with.

In [139]:
# Loading the file to a pandas DataFrame named raw. the second line shows just the first 5 rows so we can check the structure. 

raw = pd.read_pickle('stk1999.pkl')
raw.head(10)
Out[139]:
Symbol Open High Low Close Volume
Date
1999-01-01 ACU 2.2500 2.2500 2.2500 2.2500 0
1999-01-01 ACY 8.4400 8.4400 8.4400 8.4400 0
1999-01-01 AE 5.7500 5.7500 5.7500 5.7500 0
1999-01-01 AMS 1.1900 1.1900 1.1900 1.1900 0
1999-01-01 APT 0.5000 0.5000 0.5000 0.5000 0
1999-01-01 AWX 7.0600 7.0600 7.0600 7.0600 0
1999-01-01 BCV 22.7500 22.7500 22.7500 22.7500 0
1999-01-01 BDL 5.0600 5.0600 5.0600 5.0600 0
1999-01-01 BDR 6.6200 6.6200 6.6200 6.6200 0
1999-01-01 BHB 15.8333 15.8333 15.8333 15.8333 0
In [140]:
#check the type again for raw to make surte it is loaded as a DataFrame. 
print(type(raw))

#How many columns and rows are there in the data? 
print(raw.shape)
print(raw.columns)


#a detailed summary to see each columns information and types. 
raw.info()
<class 'pandas.core.frame.DataFrame'>
(526609, 6)
Index(['Symbol', 'Open', 'High', 'Low', 'Close', 'Volume'], dtype='object')
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 526609 entries, 1999-01-01 to 1999-12-31
Data columns (total 6 columns):
 #   Column  Non-Null Count   Dtype  
---  ------  --------------   -----  
 0   Symbol  526609 non-null  object 
 1   Open    526609 non-null  float64
 2   High    526609 non-null  float64
 3   Low     526609 non-null  float64
 4   Close   526609 non-null  float64
 5   Volume  526609 non-null  int64  
dtypes: float64(4), int64(1), object(1)
memory usage: 28.1+ MB
In [141]:
#this is just to see the date format
print(raw.index[:5])
print(raw.index[-5:])


#What are the unique symbols in the data? 
print(raw['Symbol'].unique())
DatetimeIndex(['1999-01-01', '1999-01-01', '1999-01-01', '1999-01-01', '1999-01-01'], dtype='datetime64[ns]', name='Date', freq=None)
DatetimeIndex(['1999-12-31', '1999-12-31', '1999-12-31', '1999-12-31', '1999-12-31'], dtype='datetime64[ns]', name='Date', freq=None)
['ACU' 'ACY' 'AE' ... 'A' 'TDY' 'CCZ']
In [143]:
# Since we will be working with MSFT and SPY. I just wanted to confirm how many times they occur in the data. 

print((raw['Symbol'] == 'MSFT').sum())
print((raw['Symbol'] == 'SPY').sum())
261
261

3. Create a clean 1999 closing price table for MSFT and SPY¶

Just to keep everything clean and being able to visualize the process I will create some tables for MSFT AND SPY before performing further calculations. This will be the base of core calucations later on.

In [148]:
#extract our target tickers and keep only the columns that we need which is the closing price column 
data_1999 = raw[raw['Symbol'].isin(['MSFT', 'SPY'])].copy()
data_1999 = data_1999[['Symbol', 'Close']]

data_1999.head()

#right now we only have MSFT and SPY after these lines
Out[148]:
Symbol Close
Date
1999-01-01 SPY 123.31
1999-01-04 SPY 123.03
1999-01-05 SPY 124.44
1999-01-06 SPY 127.44
1999-01-07 SPY 126.81
In [149]:
#we will reshape the data so that we have one column for MSFT and one column for SPY.
#I will then sort the data by date just to be sure that everything is ordrered correctly. 
prices = data_1999.pivot_table(index=data_1999.index, columns='Symbol', values='Close')
prices = prices.sort_index()
prices.head()
Out[149]:
Symbol MSFT SPY
Date
1999-01-01 34.673 123.31
1999-01-04 35.250 123.03
1999-01-05 36.625 124.44
1999-01-06 37.813 127.44
1999-01-07 37.625 126.81
In [150]:
#removing any missing rows so that both tickers will line up on the same trading day
prices = prices.dropna().copy()

print(prices.shape)
prices.head()
(261, 2)
Out[150]:
Symbol MSFT SPY
Date
1999-01-01 34.673 123.31
1999-01-04 35.250 123.03
1999-01-05 36.625 124.44
1999-01-06 37.813 127.44
1999-01-07 37.625 126.81
In [154]:
#This is to show you the final DataFrame that we will perform our calcualtions on. index start and end dates are shown and the table end is printed again for your reference. 

print(prices.index.min())
print(prices.index.max())
print(prices.columns)
prices.tail()
1999-01-01 00:00:00
1999-12-31 00:00:00
Index(['MSFT', 'SPY'], dtype='object', name='Symbol')
Out[154]:
Symbol MSFT SPY
Date
1999-12-27 59.560 146.28
1999-12-28 58.750 146.19
1999-12-29 58.970 146.81
1999-12-30 58.810 146.63
1999-12-31 58.375 146.88

4. Build the MACD(5,10) indicator for MSFT¶

Here we will create the signal for both strategies after this block of code our strategy will depend on the sign of MACD indicator

In [160]:
#create a new copy of prices as macd data. 

macd_data = prices.copy()


# this is the core of this strategy. My assumption is that MACD(5,10) = EMA(5) - EMA(10)
# the positions are +1 if MACD is higher than zero adn -1 otherwise 


#5 and 10 day EMAs are calcualted here on Microsoft. I used pandas ewm() object which calculates EMA. span =5 for 5 days, 10 for 10 days.
macd_data['EMA_5'] = macd_data['MSFT'].ewm(span=5, adjust=False).mean()
macd_data['EMA_10'] = macd_data['MSFT'].ewm(span=10, adjust=False).mean()



#MACD(5,10) is the difference between the two EMAs
macd_data['MACD'] = macd_data['EMA_5'] - macd_data['EMA_10']


macd_data.head(10)
Out[160]:
Symbol MSFT SPY EMA_5 EMA_10 MACD
Date
1999-01-01 34.673 123.31 34.673000 34.673000 0.000000
1999-01-04 35.250 123.03 34.865333 34.777909 0.087424
1999-01-05 36.625 124.44 35.451889 35.113744 0.338145
1999-01-06 37.813 127.44 36.238926 35.604518 0.634408
1999-01-07 37.625 126.81 36.700951 35.971878 0.729073
1999-01-08 37.470 127.75 36.957300 36.244264 0.713037
1999-01-11 36.875 126.53 36.929867 36.358943 0.570924
1999-01-12 35.548 124.25 36.469245 36.211499 0.257746
1999-01-13 35.953 123.38 36.297163 36.164499 0.132664
1999-01-14 35.438 121.22 36.010775 36.032408 -0.021633
In [162]:
# Let's see if these values make sense. 5 day EMA should be more responsive and MACD should be the difference.

macd_data[['MSFT', 'EMA_5', 'EMA_10', 'MACD']].head(15) 


#Everyhting looks good for now. 
Out[162]:
Symbol MSFT EMA_5 EMA_10 MACD
Date
1999-01-01 34.673 34.673000 34.673000 0.000000
1999-01-04 35.250 34.865333 34.777909 0.087424
1999-01-05 36.625 35.451889 35.113744 0.338145
1999-01-06 37.813 36.238926 35.604518 0.634408
1999-01-07 37.625 36.700951 35.971878 0.729073
1999-01-08 37.470 36.957300 36.244264 0.713037
1999-01-11 36.875 36.929867 36.358943 0.570924
1999-01-12 35.548 36.469245 36.211499 0.257746
1999-01-13 35.953 36.297163 36.164499 0.132664
1999-01-14 35.438 36.010775 36.032408 -0.021633
1999-01-15 37.438 36.486517 36.287971 0.198546
1999-01-18 37.438 36.803678 36.497067 0.306611
1999-01-19 38.908 37.505119 36.935418 0.569700
1999-01-20 40.658 38.556079 37.612251 0.943828
1999-01-21 39.578 38.896719 37.969660 0.927059

5. Strategy 1¶

In [164]:
# For strategy one, we will go long (+1) when MACD > 0, short (-1) otherwise

#this creates the +1 and -1 positions in a new column. 
macd_data['pos_s1'] = np.where(macd_data['MACD'] > 0, 1, -1)


# Close all the positions at the end of 1999
macd_data.iloc[-1, macd_data.columns.get_loc('pos_s1')] = 0

macd_data[['MSFT', 'EMA_5', 'EMA_10', 'MACD', 'pos_s1']].head(15)
Out[164]:
Symbol MSFT EMA_5 EMA_10 MACD pos_s1
Date
1999-01-01 34.673 34.673000 34.673000 0.000000 -1
1999-01-04 35.250 34.865333 34.777909 0.087424 1
1999-01-05 36.625 35.451889 35.113744 0.338145 1
1999-01-06 37.813 36.238926 35.604518 0.634408 1
1999-01-07 37.625 36.700951 35.971878 0.729073 1
1999-01-08 37.470 36.957300 36.244264 0.713037 1
1999-01-11 36.875 36.929867 36.358943 0.570924 1
1999-01-12 35.548 36.469245 36.211499 0.257746 1
1999-01-13 35.953 36.297163 36.164499 0.132664 1
1999-01-14 35.438 36.010775 36.032408 -0.021633 -1
1999-01-15 37.438 36.486517 36.287971 0.198546 1
1999-01-18 37.438 36.803678 36.497067 0.306611 1
1999-01-19 38.908 37.505119 36.935418 0.569700 1
1999-01-20 40.658 38.556079 37.612251 0.943828 1
1999-01-21 39.578 38.896719 37.969660 0.927059 1
In [166]:
# check the tail and make sure that the positions are indeed closed at the end. 

macd_data[['MSFT', 'MACD', 'pos_s1']].tail(10)
Out[166]:
Symbol MSFT MACD pos_s1
Date
1999-12-20 56.375 2.526925 1
1999-12-21 57.935 2.536527 1
1999-12-22 58.780 2.516065 1
1999-12-23 58.720 2.343325 1
1999-12-24 58.720 2.107083 1
1999-12-27 59.560 1.977794 1
1999-12-28 58.750 1.664680 1
1999-12-29 58.970 1.426334 1
1999-12-30 58.810 1.185640 1
1999-12-31 58.375 0.916586 0
In [169]:
# this counts how many times the position changes from long to short and vice versa.
# this will tell us how active our strategy is


macd_data['trade_s1'] = macd_data['pos_s1'].diff().fillna(0)
num_changes_s1 = (macd_data['trade_s1'] != 0).sum()

print("Number of position changes in Strategy 1:", num_changes_s1)
Number of position changes in Strategy 1: 22

It is important to note that the 22 here means 22 position changes, not necessarily 22 separate orders. Since the strategy flips between +1 and -1, one change can represent a reversal.

6. Backtesting Strategy 1¶

This block will allow us to see the performance of the strategy. How it compares to buy and hold and how much would have earned each day.

In [173]:
#compute daily MSFT returns
macd_data['ret_msft'] = macd_data['MSFT'].pct_change()


#  if yesterday’s position was 1, strategy return will be MSFT return today
#if yesterdays position was -1, strategy return will be the negative of MSFT return today becasue we are using yesterday's signal to trade.
macd_data['strategy1_ret'] = macd_data['pos_s1'].shift(1) * macd_data['ret_msft']


#this replaces missing first row return with 0. 
macd_data['strategy1_ret'] = macd_data['strategy1_ret'].fillna(0)


#this creates the buy and hold return which we can compare later to our return.
macd_data['buyhold_ret'] = macd_data['ret_msft'].fillna(0)

macd_data[['MSFT', 'pos_s1', 'ret_msft', 'strategy1_ret', 'buyhold_ret']].head(10)



# so in the final table, ret_msft is the daily percentage return of MSFT
# strategy1_ret is our strategy’s daily return
# buyhold_ret is the benchmark return on MSFT
Out[173]:
Symbol MSFT pos_s1 ret_msft strategy1_ret buyhold_ret
Date
1999-01-01 34.673 -1 NaN 0.000000 0.000000
1999-01-04 35.250 1 0.016641 -0.016641 0.016641
1999-01-05 36.625 1 0.039007 0.039007 0.039007
1999-01-06 37.813 1 0.032437 0.032437 0.032437
1999-01-07 37.625 1 -0.004972 -0.004972 -0.004972
1999-01-08 37.470 1 -0.004120 -0.004120 -0.004120
1999-01-11 36.875 1 -0.015879 -0.015879 -0.015879
1999-01-12 35.548 1 -0.035986 -0.035986 -0.035986
1999-01-13 35.953 1 0.011393 0.011393 0.011393
1999-01-14 35.438 -1 -0.014324 -0.014324 -0.014324
In [179]:
# this section will turn daily returns to cumultive returns. 
#so if it starts at 1 and ends at 1.20, it means that $1 grows to $1.20 which represents 20% total return. 
#we will create cumulative performaces of both to compare easily.

macd_data['cum_buyhold'] = (1 + macd_data['buyhold_ret']).cumprod()
macd_data['cum_strategy1'] = (1 + macd_data['strategy1_ret']).cumprod()


macd_data[['cum_buyhold', 'cum_strategy1']].head(10)
Out[179]:
Symbol cum_buyhold cum_strategy1
Date
1999-01-01 1.000000 1.000000
1999-01-04 1.016641 0.983359
1999-01-05 1.056297 1.021717
1999-01-06 1.090560 1.054858
1999-01-07 1.085138 1.049613
1999-01-08 1.080668 1.045289
1999-01-11 1.063508 1.028691
1999-01-12 1.025236 0.991672
1999-01-13 1.036916 1.002970
1999-01-14 1.022063 0.988603
In [180]:
# Here are the ending values for both strategies. 


print("Final buy-and-hold value:", macd_data['cum_buyhold'].iloc[-1])
print("Final Strategy 1 value:", macd_data['cum_strategy1'].iloc[-1])
Final buy-and-hold value: 1.6835866524384984
Final Strategy 1 value: 1.333540699321843

7. Plots - Strategy 1¶

We will make 3 charts for Strategy 1:

MSFT price with EMA(5) and EMA(10)

MSFT price + EMAs + position on the other axis

Buy and holf vs Strategy 1 cumulative performance

7.1 MSFT with EMA(5) and EMA(10)¶

In [184]:
#MSFT price with EMA(5) and EMA(10), raw inputs for the strategy 

plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['MSFT'], label='MSFT')
plt.plot(macd_data.index, macd_data['EMA_5'], label='EMA_5')
plt.plot(macd_data.index, macd_data['EMA_10'], label='EMA_10')

plt.title('MSFT Price with EMA(5) and EMA(10) in 1999')
plt.xlabel('Date')
plt.ylabel('Price')
plt.legend()
plt.show()
No description has been provided for this image

7.2 MSFT + EMAs + Strategy 1 position¶

In [185]:
#this shows how the strategy flips over the position over time. We can easily see when we are long and short and for how long. 

fig, ax1 = plt.subplots(figsize=(10, 6))

# Left axis shows the price and EMAs
ax1.plot(macd_data.index, macd_data['MSFT'], label='MSFT')
ax1.plot(macd_data.index, macd_data['EMA_5'], label='EMA_5')
ax1.plot(macd_data.index, macd_data['EMA_10'], label='EMA_10')
ax1.set_xlabel('Date')
ax1.set_ylabel('Price')

# Right axis shows the postition, which oscillates between -1 and 1 as seen below.
ax2 = ax1.twinx()
ax2.plot(macd_data.index, macd_data['pos_s1'], label='Position', linestyle='--')
ax2.set_ylabel('Position')
ax2.set_ylim(-1.1, 1.1)

#combining the legends 
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')

plt.title('MSFT, EMA(5), EMA(10), and Strategy 1 Position')
plt.show()
No description has been provided for this image

7.3 Cumulative performance of Strategy 1 vs buy and hold¶

In [187]:
# we just translate what we have found in cumulative performance part into a graph. 

plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['cum_buyhold'], label='Buy-and-Hold MSFT')
plt.plot(macd_data.index, macd_data['cum_strategy1'], label='Strategy 1: MACD(5,10)')

plt.title('Performance of MSFT and Strategy 1 Over Time')
plt.xlabel('Date')
plt.ylabel('Cumulative Value')
plt.legend()
plt.show()
No description has been provided for this image

7.4 Shaded long/short regions to performance chart¶

In [188]:
# This highlights the short periods, which may help visually explain where the strategy differs from buy and hold.


plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['cum_buyhold'], label='Buy-and-Hold MSFT')
plt.plot(macd_data.index, macd_data['cum_strategy1'], label='Strategy 1: MACD(5,10)')



# Shade periods where the strategy is short 
short_mask = macd_data['pos_s1'] == -1
plt.fill_between(
    macd_data.index,
    macd_data['cum_buyhold'].min(),
    macd_data[['cum_buyhold', 'cum_strategy1']].max().max(),
    where=short_mask,
    alpha=0.15
)






plt.title('Performance of MSFT and Strategy 1 Over Time')
plt.xlabel('Date')
plt.ylabel('Cumulative Value')
plt.legend()
plt.show()
No description has been provided for this image

7.5 Strategy 1 equity and cumulative maximum¶

In [189]:
# Computing Strategy 1 running maximum in this line 
macd_data['cummax_strategy1'] = macd_data['cum_strategy1'].cummax()

# drawdown for Strategy 1
macd_data['drawdown_strategy1'] = (
    macd_data['cum_strategy1'] / macd_data['cummax_strategy1'] - 1
)



# maximum drawdown date for Strategy 1
max_dd_date_s1 = macd_data['drawdown_strategy1'].idxmin()
max_dd_value_s1 = macd_data['drawdown_strategy1'].min()

# this will give us the graph that is similar to 16-7 in the textbook. 
# equity is the cumualtive value of strategy 1 
# cummax is the running maximum of that equity curve 
#we will pinpoint the max drawdown date with a vertical line


plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['cum_strategy1'], label='equity')
plt.plot(macd_data.index, macd_data['cummax_strategy1'], label='cummax')
plt.axvline(max_dd_date_s1, linestyle='--', label='max drawdown date')



plt.title('Strategy 1 Equity Curve and Running Maximum')
plt.xlabel('Date')
plt.ylabel('Cumulative Value')
plt.legend()
plt.show()



print("Strategy 1 max drawdown date:", max_dd_date_s1)
print("Strategy 1 max drawdown:", max_dd_value_s1)
No description has been provided for this image
Strategy 1 max drawdown date: 1999-06-16 00:00:00
Strategy 1 max drawdown: -0.21656153771907927

8. Strategy 2¶

Now we build the paired trading position.

When Strategy 1 says long MSFT, Strategy 2 long MSFT and short SPY with the same dollar amount.

On the last day, both positions should be 0.

In [190]:
# MSFT will be taking the same signal as Strategy 1

# SPY takes the opposite signal
macd_data['pos_s2_msft'] = macd_data['pos_s1']
macd_data['pos_s2_spy'] = -macd_data['pos_s1']

macd_data[['MSFT', 'SPY', 'MACD', 'pos_s1', 'pos_s2_msft', 'pos_s2_spy']].head(15)
Out[190]:
Symbol MSFT SPY MACD pos_s1 pos_s2_msft pos_s2_spy
Date
1999-01-01 34.673 123.31 0.000000 -1 -1 1
1999-01-04 35.250 123.03 0.087424 1 1 -1
1999-01-05 36.625 124.44 0.338145 1 1 -1
1999-01-06 37.813 127.44 0.634408 1 1 -1
1999-01-07 37.625 126.81 0.729073 1 1 -1
1999-01-08 37.470 127.75 0.713037 1 1 -1
1999-01-11 36.875 126.53 0.570924 1 1 -1
1999-01-12 35.548 124.25 0.257746 1 1 -1
1999-01-13 35.953 123.38 0.132664 1 1 -1
1999-01-14 35.438 121.22 -0.021633 -1 -1 1
1999-01-15 37.438 124.38 0.198546 1 1 -1
1999-01-18 37.438 124.38 0.306611 1 1 -1
1999-01-19 38.908 125.19 0.569700 1 1 -1
1999-01-20 40.658 126.19 0.943828 1 1 -1
1999-01-21 39.578 122.84 0.927059 1 1 -1
In [191]:
#check tail to see if the positions are closed at the year end. 
macd_data[['MACD', 'pos_s2_msft', 'pos_s2_spy']].tail(10)
Out[191]:
Symbol MACD pos_s2_msft pos_s2_spy
Date
1999-12-20 2.526925 1 -1
1999-12-21 2.536527 1 -1
1999-12-22 2.516065 1 -1
1999-12-23 2.343325 1 -1
1999-12-24 2.107083 1 -1
1999-12-27 1.977794 1 -1
1999-12-28 1.664680 1 -1
1999-12-29 1.426334 1 -1
1999-12-30 1.185640 1 -1
1999-12-31 0.916586 0 0
In [193]:
# This counts signal changes for the paired strategy as we did in strategy 1.

# Since Strategy 2 is driven by the same MACD signal as Strategy 1, this number should match Strategy 1.


macd_data['trade_s2'] = macd_data['pos_s2_msft'].diff().fillna(0)

num_changes_s2 = (macd_data['trade_s2'] != 0).sum()

print("Number of position changes in Strategy 2:", num_changes_s2)
Number of position changes in Strategy 2: 22

9. Backtesting Strategy 2¶

Now we calculate the return of the paired strategy.

Since we are investing the same dollar amount, I am treating as if we are investing +50% in one leg and investing −50% in the other leg.

In [223]:
#   daily SPY returns
macd_data['ret_spy'] = macd_data['SPY'].pct_change()


# Strategy 2 daily return

# we are using yesterday's paired positions same as strategy 1. 

# we are assigning +0.5 weight on one asset, -0.5 weight on the other asset and same dollar size on both legs
# So the whole strategy is scaled to a net starting capital of 1.0.


macd_data['strategy2_ret'] = (
    0.5 * macd_data['pos_s2_msft'].shift(1) * macd_data['ret_msft']
    + 0.5 * macd_data['pos_s2_spy'].shift(1) * macd_data['ret_spy']
)

# this replaces missing first row return with 0
macd_data['strategy2_ret'] = macd_data['strategy2_ret'].fillna(0)



macd_data[['ret_msft', 'ret_spy', 'pos_s2_msft', 'pos_s2_spy', 'strategy2_ret']].head(10)
Out[223]:
Symbol ret_msft ret_spy pos_s2_msft pos_s2_spy strategy2_ret
Date
1999-01-01 NaN NaN -1 1 0.000000
1999-01-04 0.016641 -0.002271 1 -1 -0.009456
1999-01-05 0.039007 0.011461 1 -1 0.013773
1999-01-06 0.032437 0.024108 1 -1 0.004164
1999-01-07 -0.004972 -0.004944 1 -1 -0.000014
1999-01-08 -0.004120 0.007413 1 -1 -0.005766
1999-01-11 -0.015879 -0.009550 1 -1 -0.003165
1999-01-12 -0.035986 -0.018019 1 -1 -0.008983
1999-01-13 0.011393 -0.007002 1 -1 0.009198
1999-01-14 -0.014324 -0.017507 -1 1 0.001591
In [200]:
#This turns Strategy 2 daily returns into a cumulative equity curve, same as we did for Strategy 1.
macd_data['cum_strategy2'] = (1 + macd_data['strategy2_ret']).cumprod()


macd_data[['cum_strategy2']].head(10)
Out[200]:
Symbol cum_strategy2
Date
1999-01-01 1.000000
1999-01-04 0.990544
1999-01-05 1.004187
1999-01-06 1.008369
1999-01-07 1.008355
1999-01-08 1.002540
1999-01-11 0.999368
1999-01-12 0.990390
1999-01-13 0.999499
1999-01-14 1.001089
In [203]:
# Final cumulative value of Strategy 2
print("Final Strategy 2 value:", macd_data['cum_strategy2'].iloc[-1])
Final Strategy 2 value: 1.286125708646444
In [204]:
#ending values for :
#buy and hold MSFT
# MACD on MSFT alone
# paired MACD strategy with SPY 

print("Final buy and hold value:", macd_data['cum_buyhold'].iloc[-1])
print("Final Strategy 1 value:", macd_data['cum_strategy1'].iloc[-1])
print("Final Strategy 2 value:", macd_data['cum_strategy2'].iloc[-1])
Final buy and hold value: 1.6835866524384984
Final Strategy 1 value: 1.333540699321843
Final Strategy 2 value: 1.286125708646444

10. Plots - Strategy 2¶

In [205]:
#diagrams similar to, 15-3, 16-7, 16-5

# So in this block we will makem thses charts:
#Performance comparison chart
#Strategy 2 equity curve with cumulative maximum
#Drawdown chart

10.1 Performance comparison chart for Strategy 2¶

In [207]:
plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['cum_buyhold'], label='Buy-and-Hold MSFT')
plt.plot(macd_data.index, macd_data['cum_strategy2'], label='Strategy 2: MSFT-SPY Pair')

plt.title('Performance of MSFT and Strategy 2 Over Time')
plt.xlabel('Date')
plt.ylabel('Cumulative Value')
plt.legend()
plt.show()
No description has been provided for this image

10.2 Equity curve and cumulative maximum¶

In [208]:
# cumulative strategy2 is the actual equity curve, while the cummax strategy2 is the running peak so far.
#When the equity curve falls below the cumulative max, the strategy is in drawdown.



macd_data['cummax_strategy2'] = macd_data['cum_strategy2'].cummax()

plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['cum_strategy2'], label='equity')
plt.plot(macd_data.index, macd_data['cummax_strategy2'], label='cummax')

plt.title('Strategy 2 Equity Curve and Running Maximum')
plt.xlabel('Date')
plt.ylabel('Cumulative Value')
plt.legend()
plt.show()
No description has been provided for this image

10.3 Drawdown for Strategy 2¶

In [209]:
# we will compute drawdown as a percentage below the running maximum

# Here 0 means the strategy is at a new peak.Negative values mean the strategy is below its previous peak and the most negative point is the maximum drawdown.

macd_data['drawdown_strategy2'] = (
    macd_data['cum_strategy2'] / macd_data['cummax_strategy2'] - 1
)

plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['drawdown_strategy2'], label='Drawdown')

plt.title('Strategy 2 Drawdown Over Time')
plt.xlabel('Date')
plt.ylabel('Drawdown')
plt.legend()
plt.show()
No description has been provided for this image

10.4 Vertical line at the max drawdown date¶

In [211]:
# here we find the date of maximum drawdown
max_dd_date = macd_data['drawdown_strategy2'].idxmin()
max_dd_value = macd_data['drawdown_strategy2'].min()

plt.figure(figsize=(10, 6))
plt.plot(macd_data.index, macd_data['cum_strategy2'], label='equity')
plt.plot(macd_data.index, macd_data['cummax_strategy2'], label='cummax')
plt.axvline(max_dd_date, linestyle='--', label='max drawdown date')

plt.title('Strategy 2 Equity Curve with Maximum Drawdown Date')
plt.xlabel('Date')
plt.ylabel('Cumulative Value')
plt.legend()
plt.show()

print("Max drawdown date:", max_dd_date)
print("Max drawdown:", max_dd_value)
No description has been provided for this image
Max drawdown date: 1999-06-16 00:00:00
Max drawdown: -0.08934745065196747

11. Drawdown statistics¶

Now I would like to show the maximum drawdown, and the date it occurs. the start of that drawdown period and the recovery date if it recovers within 1999

11.1 Maximum drawdown value and date¶

In [213]:
# with min(), we find the most negative drawdown, idxmin() finds the date where that worst drawdown happens
#So overall this gives us the deepest loss from a previous peak.


max_drawdown = macd_data['drawdown_strategy2'].min()
max_drawdown_date = macd_data['drawdown_strategy2'].idxmin()

print("Maximum drawdown:", max_drawdown)
print("Maximum drawdown date:", max_drawdown_date)
Maximum drawdown: -0.08934745065196747
Maximum drawdown date: 1999-06-16 00:00:00

11.2 Start date of the max drawdown period¶

In [214]:
# wjat is the peak level just before the max drawdown? 

peak_value_before_dd = macd_data.loc[:max_drawdown_date, 'cummax_strategy2'].iloc[-1]

# Find the first date where the strategy reached that peak\
# That date is the start of the max drawdown period

drawdown_start_date = macd_data.loc[:max_drawdown_date, 'cum_strategy2'][
    macd_data.loc[:max_drawdown_date, 'cum_strategy2'] == peak_value_before_dd
].index[0]

print("Drawdown start date:", drawdown_start_date)
Drawdown start date: 1999-05-14 00:00:00

11.3 Find the recovery date, if it exists¶

In [215]:
# This is where we check whether the equity curve ever climbs back to the old peak after the worst drawdown if yes, we record the first recovery date, if not, recovery is None



recovery_candidates = macd_data.loc[max_drawdown_date:, 'cum_strategy2']
recovery_candidates = recovery_candidates[recovery_candidates >= peak_value_before_dd]

if len(recovery_candidates) > 0:
    recovery_date = recovery_candidates.index[0]
else:
    recovery_date = None

print("Recovery date:", recovery_date)
Recovery date: 1999-08-24 00:00:00

11.4 Drawdown duration in trading days¶

In [217]:
# Drawdown duration in trading days. 
# we wwant to see how many days the drawdown lasted, from the peak date to the recovery date. 

if recovery_date is not None:
    drawdown_duration = macd_data.loc[drawdown_start_date:recovery_date].shape[0] - 1
else:
    drawdown_duration = macd_data.loc[drawdown_start_date:].shape[0] - 1

print("Drawdown duration (trading days):", drawdown_duration)
Drawdown duration (trading days): 72

11.5 Put drawdown statistics into a small table¶

In [218]:
# we put everything that we calculated in a table. 

drawdown_summary = pd.DataFrame({
    'Metric': [
        'Maximum Drawdown',
        'Max Drawdown Date',
        'Drawdown Start Date',
        'Recovery Date',
        'Drawdown Duration (days)'
    ],
    'Value': [
        max_drawdown,
        max_drawdown_date,
        drawdown_start_date,
        recovery_date,
        drawdown_duration
    ]
})



drawdown_summary
Out[218]:
Metric Value
0 Maximum Drawdown -0.089347
1 Max Drawdown Date 1999-06-16 00:00:00
2 Drawdown Start Date 1999-05-14 00:00:00
3 Recovery Date 1999-08-24 00:00:00
4 Drawdown Duration (days) 72

12. Summary table of results¶

Now we will create one final table comparing the three approaches:

Buy and Hold MSFT

Strategy 1 - MACD on MSFT

Strategy 2 - MACD on MSFT + opposite SPY trade

We will include: the final value, total return, annualized return, annualized volatility, maximum drawdown

12.1 Calculate the performance statistics¶

In [220]:
#How many trading days do we have in this sample? 


n_days = len(macd_data)


# Annualization factor for daily data
annual_factor = 252


# Maximum drawdown for buy and hold and Strategy 1, we have already computed it for Strategy 2 so we don;t need to do it again. 


macd_data['cummax_buyhold'] = macd_data['cum_buyhold'].cummax()
macd_data['drawdown_buyhold'] = macd_data['cum_buyhold'] / macd_data['cummax_buyhold'] - 1

macd_data['cummax_strategy1'] = macd_data['cum_strategy1'].cummax()
macd_data['drawdown_strategy1'] = macd_data['cum_strategy1'] / macd_data['cummax_strategy1'] - 1

12.2 Summary table¶

In [222]:
summary_table = pd.DataFrame({
    'Final Value': [
        macd_data['cum_buyhold'].iloc[-1],
        macd_data['cum_strategy1'].iloc[-1],
        macd_data['cum_strategy2'].iloc[-1]
    ],
    'Total Return': [
        macd_data['cum_buyhold'].iloc[-1] - 1,
        
        macd_data['cum_strategy1'].iloc[-1] - 1,
        macd_data['cum_strategy2'].iloc[-1] - 1
    ],

    
    'Annualized Return': [
        macd_data['cum_buyhold'].iloc[-1] ** (annual_factor / n_days) - 1,
        
        macd_data['cum_strategy1'].iloc[-1] ** (annual_factor / n_days) - 1,
        macd_data['cum_strategy2'].iloc[-1] ** (annual_factor / n_days) - 1
    ],



    
    'Annualized Volatility': [
        macd_data['buyhold_ret'].std() * np.sqrt(annual_factor),
        macd_data['strategy1_ret'].std() * np.sqrt(annual_factor),
        macd_data['strategy2_ret'].std() * np.sqrt(annual_factor)
        
    ],

    
    'Sharpe-like Ratio': [
        macd_data['buyhold_ret'].mean() / macd_data['buyhold_ret'].std() * np.sqrt(annual_factor),
        macd_data['strategy1_ret'].mean() / macd_data['strategy1_ret'].std() * np.sqrt(annual_factor),
        macd_data['strategy2_ret'].mean() / macd_data['strategy2_ret'].std() * np.sqrt(annual_factor)
    ],

    
    'Maximum Drawdown': [
        macd_data['drawdown_buyhold'].min(),
        macd_data['drawdown_strategy1'].min(),
        macd_data['drawdown_strategy2'].min()
    ]
}, index=[
    'Buy-and-Hold MSFT',
    'Strategy 1',
    'Strategy 2'
])

summary_table



summary_table_rounded = summary_table.copy()
summary_table_rounded = summary_table_rounded.round(4)

summary_table_rounded
Out[222]:
Final Value Total Return Annualized Return Annualized Volatility Sharpe-like Ratio Maximum Drawdown
Buy-and-Hold MSFT 1.6836 0.6836 0.6536 0.3741 1.5308 -0.1969
Strategy 1 1.3335 0.3335 0.3204 0.3752 0.9272 -0.2166
Strategy 2 1.2861 0.2861 0.2750 0.1522 1.6730 -0.0893
In [ ]:
 
In [ ]: