Python application 4: price signals

Not a chartist by any stretch, but this application illustrates algorithmic approaches to trading by identifying moving average crossings as potential price signals. (A few more lines of codes suffice to identify crossings of different moving averages such as golden crosses.) One can connect those sorts of calculations with interactive broker's python API to complete the trading algorithm. There are oodles of libraries that do this, more, and do it better. The goal here is merely illustration.

In [41]:
import pandas as pd # pandas is excellent for creating and manipulating dataframes, R-style
import numpy as np # great for simulations, we may not use for running regressions here
import matplotlib.pyplot as plt #graphing module with matlab-like properties
%matplotlib inline 
import requests # to make requests to webpages (like mine)
import statsmodels.api as sm # module full of standard statistical and econometric models, including OLS and time-series stuff
from IPython.display import Latex # to be able to display latex in python command cells
from pandas_datareader.data import DataReader # datereader downloads from all kinds of places including yahoo and google finance
import datetime

Let's get some daily closing price data for our good friend IBM. And then let's add a 30-day moving average. Let's also get rid of the 30-day burn-in data and plot the result.

In [42]:
# get 10 years or so of adjusted close price data for IBM 

end=datetime.datetime.today()
IBMdata=DataReader('IBM', 'yahoo', '2020-01-01', end)['Adj Close'] 
IBMdataF=IBMdata.to_frame() # converts the series format to a frame format which, for me, makes manipulations easier
IBMdataF.rename(columns={'Adj Close':'IBM'},inplace=True)
IBMdataF['30day_MA'] = IBMdataF['IBM'].rolling(window=30).mean()
IBMdataF.dropna(subset=['30day_MA'], inplace=True)

IBMdataF.plot(y=['IBM','30day_MA'])
Out[42]:
<matplotlib.axes._subplots.AxesSubplot at 0x1f0c4216190>

Now let's locate the signals and plot them on our chart. Again, if this was a proper buy signal we could feed it into an algorithmic trading routine using, say, Interactive Brokers' Python API.

In [43]:
IBMdataF['Gap']=IBMdataF['IBM']-IBMdataF['30day_MA'] # positive if price is above 30 day MA
IBMdataF['GapLag']=IBMdataF.Gap.shift(1) # same previous trading day

IBMdataF['Signal']=np.where(IBMdataF.Gap < 0, False, (np.where(IBMdataF.GapLag < 0,True,False))) # a crossing happens where gap is negative yesterday but positive today

# let's mark buy signals with an arrow

fig= plt.figure()
ax = fig.add_subplot(111)

ax.plot(IBMdataF.index,IBMdataF['IBM'])
ax.plot(IBMdataF.index,IBMdataF['30day_MA'])
fig.autofmt_xdate()

IBMdataBuy=IBMdataF[IBMdataF['Signal'] == True] # this just keeps the places where a buy signal is found


arrow_properties = dict(
    facecolor="blue", width=0.1,
    headwidth=4, shrink=0.1)


for date in IBMdataBuy.index:
    arrow_x = date
    arrow_y = IBMdataBuy.loc[date,'IBM']
    # print(arrow_x,arrow_y)
    
    ax.annotate(
   '', xy=(arrow_x, arrow_y),
     xytext=(0, 25), textcoords='offset points', 
                arrowprops=dict(arrowstyle="<|-"))
In [44]:
IBMdataBuy
Out[44]:
IBM 30day_MA Gap GapLag Signal
Date
2020-04-07 111.953064 111.561839 0.391225 -0.594795 True
2020-05-18 119.988930 118.166681 1.822248 -2.151470 True
2020-05-26 120.186356 118.728340 1.458015 -1.795617 True
2020-06-16 123.532539 121.798501 1.734038 -1.553573 True
2020-07-15 121.410324 120.726280 0.684044 -1.783644 True
2020-08-24 125.680000 124.080803 1.599197 -0.653447 True
2020-08-27 124.650002 124.434172 0.215829 -0.189416 True
2020-09-02 128.179993 124.443917 3.736076 -1.004817 True
2020-09-16 124.220001 124.124622 0.095380 -1.684406 True
2020-10-07 124.070000 122.271000 1.799000 -0.319998 True
In [ ]:
 

Now in practice we would also want to back-test this "strategy." We would for instance expect that the mean return on the day following a buy signal is significantly higher than on other days. So we are asking: assume I buy at the close of crossing days and sell at the close of the next day, do I generate an average return that is significantly different from the return I average by buying on other days?

Proper financial tests take a number of other factors into account but, once again for illustration, we will perform a simple mean test. For that, we begin by downloading longer data and then proceed as before. Not surprisingly given the naivete of our approach, we don't get much of a t-stat and and we get high p-value. We can't reject the hypothesis that returns are the same on average on buy days than on other days with much confidence. Worse, we get a negative return on average. So this notebook is not going to make us rich.

In [45]:
from scipy.stats import ttest_ind

end=datetime.datetime.today()
IBMdata=DataReader('IBM', 'yahoo', '2010-01-01', end)['Adj Close'] 
IBMdataF=IBMdata.to_frame() # converts the series format to a frame format which, for me, makes manipulations easier
IBMdataF.rename(columns={'Adj Close':'IBM'},inplace=True)
IBMdataF['30day_MA'] = IBMdataF['IBM'].rolling(window=30).mean()
IBMdataF.dropna(subset=['30day_MA'], inplace=True)

IBMdataF['Gap']=IBMdataF['IBM']-IBMdataF['30day_MA'] # positive if price is above 30 day MA
IBMdataF['GapLag']=IBMdataF.Gap.shift(1) # same previous trading day

IBMdataF['Signal']=np.where(IBMdataF.Gap < 0, False, (np.where(IBMdataF.GapLag < 0,True,False))) # a crossing happens where gap is negative yesterday but positive today

IBMdataF['rIBM']=-np.log(IBMdataF['IBM'])+np.log(IBMdataF.IBM.shift(-1)) #computes the one day-ahead return based on adjusted series for IBM 

IBMdataF.dropna(subset=['rIBM'], inplace=True)

Buy = IBMdataF[IBMdataF['Signal']==True]
DontBuy = IBMdataF[IBMdataF['Signal']==False]

ttest_ind(Buy['rIBM'], DontBuy['rIBM'])
Out[45]:
Ttest_indResult(statistic=-0.8671712616874857, pvalue=0.385925582981105)