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 [91]:
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 [68]:
# 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[68]:
<matplotlib.axes._subplots.AxesSubplot at 0x2565956d760>

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 [69]:
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

IBMdataF['datenow']=IBMdataF.index

fig, ax = plt.subplots()

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['datenow']:
    arrow_x = IBMdataBuy.loc[date,'datenow']
    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="<|-"))
    

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.

In [90]:
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 monthly 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[90]:
Ttest_indResult(statistic=1.3218033277424537, pvalue=0.18634752607294447)