# Backtesting exercises in Python

Li Yuqiong_BacktestingExercises

### Exercises for backtesting¶

##### Updates from Previous version on 2017 Mar 2:¶
1. Added calculation of monthly log return and volatility.

Current version of this document demonstrates a Python script to implement two trading strategies based on historical daily end stock data.

The stock data containing historical end price is downloaded from Yahoo! Finance using Pandas. This script contains a list of S&P500 ticker data and thus can analyze the performance on these listed companies.

Everyday’s trading state is characterized by four variables:

1. “Share” = number of shares of each stock symbol. The value takes integers only.
2. “Cash” = cash available for buying stocks.
3. “Position” = Share * Cash. The value of stocks at that day’s end price.
4. “Total” = Cash + Position. The net asset value of trading account at the date according to stock’s daily end price.

1. The trigger point is, once encounter a 5% decrease of initial day end price within the month, cut 100% position the next day. Buy back the same stock using all available cash at the beginning of next month, and repeat the process.
2. First trigger point is, cut 20% position at 5% loss on initial day end price based on day end price within every month. Then, if encounter another 5% loss on day end price within the same month, cut 100% of position the next day. Buy back the same stock using all available cash at the beginning of next month, and repeat the process.

In the end, this version of the script outputs a Pandas Panel object with three dimension being 1) ticker symbol 2) trading date and 3) net asset value (i.e. “Total”) at the end of each day using strategy 1 and strategy 2.

With the output panel object, the remaining work is to do a statistical analysis on the structural performance of these strategies, and the optimal allocation of capital on different investment options. This part of analysis is not included in this version of the code.

Also, some of the indexing methods in this version of code is not the most efficient way. This can be improved in future versions of the codes.

Finally, for demonstration, initial setup in this program is Share = 0, Total = 10000.0

The testing period is from 2015-1-1 to 2015-03-01

The stocks being tested are “ABBV”, “ABT”, “MMM”

In [4]:
import numpy as np
import pandas as pd  # pandas for data storage and analysis
import pandas_datareader.data as web # this module provides function to fetch data from yahoo finance
import datetime
import requests, bs4 # for web scrapping


The section below includes self-defined functions used in constructing trading strategies. These actions include: get ticker list from Wikipedia; get monthly data based on historical daily end price; check for trigger dates; initialize each month’s first state of Share and Cash; perform buying, selling and holding actions.

Explainations are included before each function.

In [5]:
### Function name: getSP500.
### Dependencies: requests, bs4
### Note: This function scapes S&P500 ticker symbols from wikipedia
###### and store them in a list
def getSP500():
url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
res = requests.get(url)
res.raise_for_status()
total = bs4.BeautifulSoup(res.text, "lxml")
elems = total.select('a[href^="https://www.nyse.com/quote/"]')
ticker = [x.getText() for x in elems]
return(ticker);

### Function name: getMonthlyData
### Dependencies: pandas
### Note: This function takes in a pandas dataframe with all data and
##### return slices of it based on year and month
def getMonthlyData(df, year, month):
dat = df.loc[(df.index.year==year) & (df.index.month == month)]
return(dat);

### Function name: checkCut
### Dependencies: pandas
### Note: This function takes in a pandas dataframe with monthly data
##### return dates of loss trigger and cut
def checkCut(dat, percent):
threshold1 = dat.Close[0] * (1-percent)
if dat.loc[dat.Close <= threshold1].empty:
return {};
else:
triggerDate1 = dat.index[dat.Close <= threshold1][0]
if (triggerDate1 == dat.index[-1]):
return {}; # if triggerDate1 is the last day of the month, do not cut
else:
cutDate1 = dat.ix[dat.index.get_loc(triggerDate1)+1].name
datslice = dat[triggerDate1: ]
threshold2 = dat.Close[0] * (1-percent)* (1-percent)
if datslice.loc[datslice.Close <= threshold2].empty:
return {"triggerDate1": triggerDate1, "cutDate1": cutDate1};
else:
triggerDate2 = datslice.index[datslice.Close <= threshold2][0]
if (triggerDate2 == datslice.index[-1]):
return {"triggerDate1": triggerDate1, "cutDate1": cutDate1};
else:
cutDate2 = datslice.ix[datslice.index.get_loc(triggerDate2)+1].name
return {"triggerDate1": triggerDate1, "cutDate1": cutDate1,
"triggerDate2": triggerDate2, "cutDate2": cutDate2};

### Function name: Cut
### Dependencies: pandas
### Note: This function takes in a pandas dataframe with monthly data and indexes
##### return an updated dataframe slices where stock from index1 are cut at index2
def Cut(dat, index1, index2, percent):
dat.Share[index2] = np.floor(dat.Position[index1] * (1-percent) /dat.Close[index2])
dat.Cash[index2] = dat.Cash[index1] + (dat.Share[index1] - dat.Share[index2]) * dat.Close[index2]
dat.Position[index2] = dat.Share[index2] * dat.Close[index2]
dat.Total[index2] = dat.Position[index2] + dat.Cash[index2]
return(dat);

### Function name: Hold
### Dependencies: pandas
### Note: This function takes in a pandas dataframe with monthly data and indexes
##### return an updated dataframe slices where stock are hold from index1 to index2
def Hold(dat, index1, index2):
dat.Share[index1:index2] = dat.Share[index1]
dat.Cash[index1:index2] = dat.Cash[index1]
dat.Position[index1:index2] = dat.Share[index1:index2] * dat.Close[index1:index2]
dat.Total[index1:index2] = dat.Position[index1:index2] + dat.Cash[index1:index2]
return(dat);


Caveats for the codes below: this version of getPrevious() function works for Year = 2015 only! A further revision is to modify parameters to make manual adjustment of years in this chunk of code unnecessary.

In [6]:
### Function name: getPrevious
### Dependencies: pandas
### Note: This function takes in 1) a pandas dataframe containing all historical data and 2) year and month to fetch data
##### return an the initial state of stock share and cash from last month
### Caveats: Beginning of months are tricky
def getPrevious(df, year, month):
if ((year == df.index.year[0]) & (month == df.index.month[0])):
inShare = 0.0
inCash = 10000.0
elif (month == 1):
inShare = df.Share[(df.index.year==year-1) & (df.index.month == 12)].tail(1)[0]
inCash = df.Cash[(df.index.year==year-1) & (df.index.month == 12)].tail(1)[0]
else:
inShare = df.Share[(df.index.year==year) & (df.index.month == month-1)].tail(1)[0]
inCash = df.Cash[(df.index.year==year) & (df.index.month == month-1)].tail(1)[0]
return([inShare, inCash]);

In [7]:
### Function name: firstDay
### Dependencies: pandas, getPrevious function
### Note: This function takes in 1) monthly data to be updated 2) initial states of cash and share
##### return the first day position of each month, where supposed to buy back
def firstDay(dat, inShare, inCash):
dat.Share[0] = inShare + np.floor(inCash/dat.Close[0])
dat.Cash[0] = inCash - (dat.Share[0]-inShare) * dat.Close[0]
dat.Position[0] = dat.Close[0] * dat.Share[0]
dat.Total[0] = dat.Position[0] + dat.Cash[0]
return(dat);


This section below includes codes to perform strategy 1 and strategy 2 as described at the beginning.

In [8]:
### Function name: getAsset1
### Dependencies: pandas, getPrevious, getMonthlyData, firstDay, Hold, Cut
### Note: This function takes in monthly data to be updated
##### return the updated data slice after executing strategy 1
def getAsset1(df):
for year in range(df.index.year[0], df.index.year[-1]+1):
for month in range(df.loc[df.index.year==year].index.month[0], df.loc[df.index.year==year].index.month[-1]+1):
dat = getMonthlyData(df, year, month)
inShare = getPrevious(df, year, month)[0]
inCash = getPrevious(df, year, month)[1]
dat = firstDay(dat, inShare, inCash)
beginDate = dat.index[0]
endDate = dat.index[-1]
if (len(checkCut(dat, percent))==0):
Hold(dat, beginDate, endDate)
else:
triggerDate1 = checkCut(dat, percent)['triggerDate1']
cutDate1 = checkCut(dat, percent)['cutDate1']
Hold(dat, beginDate, triggerDate1)
Cut(dat, triggerDate1, cutDate1, s1cutPt1)
Hold(dat, cutDate1, endDate)
df.loc[(df.index.year==year) & (df.index.month == month)] = dat
return(df);

### Function name: getAsset2
### Dependencies: pandas, getPrevious, getMonthlyData, firstDay, Hold, Cut
### Note: This function takes in monthly data to be updated
##### return the updated data slice after executing strategy 2
def getAsset2(df):
for year in range(df.index.year[0], df.index.year[-1]+1):
for month in range(df.loc[df.index.year==year].index.month[0], df.loc[df.index.year==year].index.month[-1]+1):
dat = getMonthlyData(df, year, month)
inShare = getPrevious(df, year, month)[0]
inCash = getPrevious(df, year, month)[1]
dat = firstDay(dat, inShare, inCash)
beginDate = dat.index[0]
endDate = dat.index[-1]
if (len(checkCut(dat, percent))==0):
Hold(dat, beginDate, endDate)
elif (len(checkCut(dat, percent))==2):
triggerDate1 = checkCut(dat, percent)['triggerDate1']
cutDate1 = checkCut(dat, percent)['cutDate1']
Hold(dat, beginDate, triggerDate1)
Cut(dat, triggerDate1, cutDate1, s2cutPt1)
Hold(dat, cutDate1, endDate)
else:
triggerDate1 = checkCut(dat, percent)['triggerDate1']
cutDate1 = checkCut(dat, percent)['cutDate1']
triggerDate2 = checkCut(dat, percent)['triggerDate2']
cutDate2 = checkCut(dat, percent)['cutDate2']
Hold(dat, beginDate, triggerDate1)
Cut(dat, triggerDate1, cutDate1, s2cutPt1)
Hold(dat, cutDate1, triggerDate2)
Cut(dat, triggerDate2, cutDate2, s2cutPt2)
Hold(dat, cutDate2, endDate)
df.loc[(df.index.year==year) & (df.index.month == month)] = dat
return(df);

##### Experiment¶

This part prepares and cleans data available for analysis.

Again, for demonstration, initial setup in this program is Share = 0, Total = 10000.0

The testing period is from 2002-11-16 to 2005-04-24. These two dates are a Saturday and a Sunday respectively. To change the period of backtesting, modify the start and end date is enough.

The stocks being tested are “MSFT” Microsoft and “AMZN” Amazon. This code works for other tickers listed on S&P500 as well.

In [9]:
### From here start stat analysis on strategies
start = datetime.datetime(2002, 11, 16) # This is a Saturaday
end = datetime.datetime(2005, 4, 24) # This is a Sunday

# start = datetime.datetime(1998, 3, 14) # Another test date
# end = datetime.datetime(2001, 1, 1)

# ticker = getSP500()[0:3]
ticker = ['MSFT', 'AMZN']

f = web.DataReader(ticker, "yahoo", start, end)

In [10]:
pnl = f.transpose(2, 1, 0)
pnl = pnl.drop(['Open', 'High', 'Low', 'Volume', 'Adj Close'], 2)
pnl.loc[:, :, 'Share'] = 0.0
pnl.loc[:, :, 'Position'] = 0.0
pnl.loc[:, :, 'Cash'] = 10000.0
pnl.loc[:, :, 'Total'] = 10000.0

percent = 0.05
s1cutPt1 = 1
s2cutPt1 = 0.2
s2cutPt2 = 1


This part of the codes implements the previous two trading strategies on data, collect results and organize it into a sigle output object.

In [11]:
s1pnl = pnl.apply(lambda x:getAsset1(x), axis=(1, 2)) # results of strategy 1
s2pnl = pnl.apply(lambda x:getAsset2(x), axis=(1, 2)) # results of strategy 2

/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:11: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/pandas/core/series.py:684: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
self._set_with(key, value)
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:19: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:20: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:21: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:15: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:35: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:43: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:44: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:45: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:51: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:52: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:53: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:54: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:55: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
/anaconda/lib/python2.7/site-packages/ipykernel/__main__.py:39: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

###### Get a detailed view of each stock’s specific performance for each strategy¶

The charts below shows the daily changes in trading account, including shares, cash, position and total money. There thus should be four tables: Amazon for strategy one and two, Microsoft for strategy one and two.

In [12]:
s1pnl.loc['AMZN'] # Performance for Amazon at Strategy one

Out[12]:
Close Share Position Cash Total
Date
2002-11-18 22.420000 446 9999.320000 0.680000 10000.000000
2002-11-19 21.290001 446 9495.340446 0.680000 9496.020446
2002-11-20 22.900000 0 0.000000 10214.080000 10214.080000
2002-11-21 23.400000 0 0.000000 10214.080000 10214.080000
2002-11-22 23.990000 0 0.000000 10214.080000 10214.080000
2002-11-25 24.250000 0 0.000000 10214.080000 10214.080000
2002-11-26 23.400000 0 0.000000 10214.080000 10214.080000
2002-11-27 24.080000 0 0.000000 10214.080000 10214.080000
2002-11-29 23.350000 0 0.000000 10214.080000 10214.080000
2002-12-02 24.110001 423 10198.530423 15.549577 10214.080000
2002-12-03 23.740000 423 10042.020000 15.549577 10057.569577
2002-12-04 23.389999 423 9893.969577 15.549577 9909.519154
2002-12-05 22.580000 423 9551.340000 15.549577 9566.889577
2002-12-06 22.610001 0 0.000000 9579.580000 9579.580000
2002-12-09 21.680000 0 0.000000 9579.580000 9579.580000
2002-12-10 21.860001 0 0.000000 9579.580000 9579.580000
2002-12-11 22.129999 0 0.000000 9579.580000 9579.580000
2002-12-12 22.299999 0 0.000000 9579.580000 9579.580000
2002-12-13 22.180000 0 0.000000 9579.580000 9579.580000
2002-12-16 22.510000 0 0.000000 9579.580000 9579.580000
2002-12-17 22.459999 0 0.000000 9579.580000 9579.580000
2002-12-18 22.100000 0 0.000000 9579.580000 9579.580000
2002-12-19 21.690001 0 0.000000 9579.580000 9579.580000
2002-12-20 21.930000 0 0.000000 9579.580000 9579.580000
2002-12-23 22.240000 0 0.000000 9579.580000 9579.580000
2002-12-24 21.879999 0 0.000000 9579.580000 9579.580000
2002-12-26 20.299999 0 0.000000 9579.580000 9579.580000
2002-12-27 18.860001 0 0.000000 9579.580000 9579.580000
2002-12-30 19.250000 0 0.000000 9579.580000 9579.580000
2002-12-31 18.889999 0 0.000000 9579.580000 9579.580000
2005-03-11 34.750000 367 12753.250000 31.409562 12784.659562
2005-03-14 34.599998 367 12698.199266 31.409562 12729.608828
2005-03-15 34.180000 367 12544.060000 31.409562 12575.469562
2005-03-16 33.869999 367 12430.289633 31.409562 12461.699195
2005-03-17 33.970001 367 12466.990367 31.409562 12498.399929
2005-03-18 34.160000 367 12536.720000 31.409562 12568.129562
2005-03-21 33.669998 367 12356.889266 31.409562 12388.298828
2005-03-22 33.150002 367 12166.050734 31.409562 12197.460296
2005-03-23 33.180000 0 0.000000 12208.469562 12208.469562
2005-03-24 32.880001 0 0.000000 12208.469562 12208.469562
2005-03-28 33.389999 0 0.000000 12208.469562 12208.469562
2005-03-29 33.430000 0 0.000000 12208.469562 12208.469562
2005-03-30 34.320000 0 0.000000 12208.469562 12208.469562
2005-03-31 34.270000 0 0.000000 12208.469562 12208.469562
2005-04-01 34.009998 358 12175.579284 32.890278 12208.469562
2005-04-04 35.000000 358 12530.000000 32.890278 12562.890278
2005-04-05 35.279999 358 12630.239642 32.890278 12663.129920
2005-04-06 34.820000 358 12465.560000 32.890278 12498.450278
2005-04-07 34.900002 358 12494.200716 32.890278 12527.090994
2005-04-08 34.599998 358 12386.799284 32.890278 12419.689562
2005-04-11 34.599998 358 12386.799284 32.890278 12419.689562
2005-04-12 34.630001 358 12397.540358 32.890278 12430.430636
2005-04-13 34.299999 358 12279.399642 32.890278 12312.289920
2005-04-14 33.970001 358 12161.260358 32.890278 12194.150636
2005-04-15 33.020000 358 11821.160000 32.890278 11854.050278
2005-04-18 33.000000 358 11814.000000 32.890278 11846.890278
2005-04-19 32.779999 358 11735.239642 32.890278 11768.129920
2005-04-20 33.250000 358 11903.500000 32.890278 11936.390278
2005-04-21 33.730000 358 12075.340000 32.890278 12108.230278
2005-04-22 33.040001 358 11828.320358 32.890278 11861.210636

611 rows × 5 columns

In [13]:
s1pnl.loc['MSFT'] # Performance for Microsoft at Strategy one

Out[13]:
Close Share Position Cash Total
Date
2002-11-18 55.849998 179 9997.149642 2.850358 10000.000000
2002-11-19 54.860001 179 9819.940179 2.850358 9822.790537
2002-11-20 56.619999 179 10134.979821 2.850358 10137.830179
2002-11-21 57.840000 179 10353.360000 2.850358 10356.210358
2002-11-22 58.220001 179 10421.380179 2.850358 10424.230537
2002-11-25 58.230000 179 10423.170000 2.850358 10426.020358
2002-11-26 56.900002 179 10185.100358 2.850358 10187.950716
2002-11-27 58.080002 179 10396.320358 2.850358 10399.170716
2002-11-29 57.680000 179 10324.720000 2.850358 10327.570358
2002-12-02 57.689999 179 10326.509821 2.850358 10329.360179
2002-12-03 56.709999 179 10151.089821 2.850358 10153.940179
2002-12-04 56.540001 179 10120.660179 2.850358 10123.510537
2002-12-05 55.340000 179 9905.860000 2.850358 9908.710358
2002-12-06 55.470001 179 9929.130179 2.850358 9931.980537
2002-12-09 53.529999 179 9581.869821 2.850358 9584.720179
2002-12-10 54.009998 0 0.000000 9670.640000 9670.640000
2002-12-11 54.660000 0 0.000000 9670.640000 9670.640000
2002-12-12 54.169998 0 0.000000 9670.640000 9670.640000
2002-12-13 52.500000 0 0.000000 9670.640000 9670.640000
2002-12-16 54.480000 0 0.000000 9670.640000 9670.640000
2002-12-17 54.360001 0 0.000000 9670.640000 9670.640000
2002-12-18 53.529999 0 0.000000 9670.640000 9670.640000
2002-12-19 53.110001 0 0.000000 9670.640000 9670.640000
2002-12-20 53.040001 0 0.000000 9670.640000 9670.640000
2002-12-23 54.000000 0 0.000000 9670.640000 9670.640000
2002-12-24 53.820000 0 0.000000 9670.640000 9670.640000
2002-12-26 53.389999 0 0.000000 9670.640000 9670.640000
2002-12-27 52.970001 0 0.000000 9670.640000 9670.640000
2002-12-30 52.750000 0 0.000000 9670.640000 9670.640000
2002-12-31 51.700001 0 0.000000 9670.640000 9670.640000
2005-03-11 25.090000 189 4742.010000 17.739829 4759.749829
2005-03-14 25.110001 189 4745.790189 17.739829 4763.530018
2005-03-15 24.910000 189 4707.990000 17.739829 4725.729829
2005-03-16 24.629999 189 4655.069811 17.739829 4672.809640
2005-03-17 24.540001 189 4638.060189 17.739829 4655.800018
2005-03-18 24.309999 189 4594.589811 17.739829 4612.329640
2005-03-21 24.200001 189 4573.800189 17.739829 4591.540018
2005-03-22 23.990000 189 4534.110000 17.739829 4551.849829
2005-03-23 24.180000 0 0.000000 4587.759829 4587.759829
2005-03-24 24.280001 0 0.000000 4587.759829 4587.759829
2005-03-28 24.200001 0 0.000000 4587.759829 4587.759829
2005-03-29 23.920000 0 0.000000 4587.759829 4587.759829
2005-03-30 24.160000 0 0.000000 4587.759829 4587.759829
2005-03-31 24.170000 0 0.000000 4587.759829 4587.759829
2005-04-01 24.120001 190 4582.800190 4.959639 4587.759829
2005-04-04 24.230000 190 4603.700000 4.959639 4608.659639
2005-04-05 24.469999 190 4649.299810 4.959639 4654.259449
2005-04-06 24.670000 190 4687.300000 4.959639 4692.259639
2005-04-07 25.100000 190 4769.000000 4.959639 4773.959639
2005-04-08 24.940001 190 4738.600190 4.959639 4743.559829
2005-04-11 24.969999 190 4744.299810 4.959639 4749.259449
2005-04-12 25.320000 190 4810.800000 4.959639 4815.759639
2005-04-13 25.040001 190 4757.600190 4.959639 4762.559829
2005-04-14 24.840000 190 4719.600000 4.959639 4724.559639
2005-04-15 24.459999 190 4647.399810 4.959639 4652.359449
2005-04-18 24.650000 190 4683.500000 4.959639 4688.459639
2005-04-19 24.629999 190 4679.699810 4.959639 4684.659449
2005-04-20 24.320000 190 4620.800000 4.959639 4625.759639
2005-04-21 25.280001 190 4803.200190 4.959639 4808.159829
2005-04-22 24.980000 190 4746.200000 4.959639 4751.159639

611 rows × 5 columns

In [14]:
s2pnl.loc['AMZN'] # Performance for Amazon at Strategy two

Out[14]:
Close Share Position Cash Total
Date
2002-11-18 22.420000 446 9999.320000 0.680000 10000.000000
2002-11-19 21.290001 446 9495.340446 0.680000 9496.020446
2002-11-20 22.900000 331 7579.900000 2634.180000 10214.080000
2002-11-21 23.400000 331 7745.400000 2634.180000 10379.580000
2002-11-22 23.990000 331 7940.690000 2634.180000 10574.870000
2002-11-25 24.250000 331 8026.750000 2634.180000 10660.930000
2002-11-26 23.400000 331 7745.400000 2634.180000 10379.580000
2002-11-27 24.080000 331 7970.480000 2634.180000 10604.660000
2002-11-29 23.350000 331 7728.850000 2634.180000 10363.030000
2002-12-02 24.110001 440 10608.400440 6.189891 10614.590331
2002-12-03 23.740000 440 10445.600000 6.189891 10451.789891
2002-12-04 23.389999 440 10291.599560 6.189891 10297.789451
2002-12-05 22.580000 440 9935.200000 6.189891 9941.389891
2002-12-06 22.610001 351 7936.110351 2018.479980 9954.590331
2002-12-09 21.680000 351 7609.680000 2018.479980 9628.159980
2002-12-10 21.860001 0 0.000000 9691.340331 9691.340331
2002-12-11 22.129999 0 0.000000 9691.340331 9691.340331
2002-12-12 22.299999 0 0.000000 9691.340331 9691.340331
2002-12-13 22.180000 0 0.000000 9691.340331 9691.340331
2002-12-16 22.510000 0 0.000000 9691.340331 9691.340331
2002-12-17 22.459999 0 0.000000 9691.340331 9691.340331
2002-12-18 22.100000 0 0.000000 9691.340331 9691.340331
2002-12-19 21.690001 0 0.000000 9691.340331 9691.340331
2002-12-20 21.930000 0 0.000000 9691.340331 9691.340331
2002-12-23 22.240000 0 0.000000 9691.340331 9691.340331
2002-12-24 21.879999 0 0.000000 9691.340331 9691.340331
2002-12-26 20.299999 0 0.000000 9691.340331 9691.340331
2002-12-27 18.860001 0 0.000000 9691.340331 9691.340331
2002-12-30 19.250000 0 0.000000 9691.340331 9691.340331
2002-12-31 18.889999 0 0.000000 9691.340331 9691.340331
2005-03-11 34.750000 479 16645.250000 17.680233 16662.930233
2005-03-14 34.599998 479 16573.399042 17.680233 16591.079275
2005-03-15 34.180000 479 16372.220000 17.680233 16389.900233
2005-03-16 33.869999 479 16223.729521 17.680233 16241.409754
2005-03-17 33.970001 479 16271.630479 17.680233 16289.310712
2005-03-18 34.160000 479 16362.640000 17.680233 16380.320233
2005-03-21 33.669998 479 16127.929042 17.680233 16145.609275
2005-03-22 33.150002 479 15878.850958 17.680233 15896.531191
2005-03-23 33.180000 382 12674.760000 3236.140233 15910.900233
2005-03-24 32.880001 382 12560.160382 3236.140233 15796.300615
2005-03-28 33.389999 382 12754.979618 3236.140233 15991.119851
2005-03-29 33.430000 382 12770.260000 3236.140233 16006.400233
2005-03-30 34.320000 382 13110.240000 3236.140233 16346.380233
2005-03-31 34.270000 382 13091.140000 3236.140233 16327.280233
2005-04-01 34.009998 477 16222.769046 5.190423 16227.959469
2005-04-04 35.000000 477 16695.000000 5.190423 16700.190423
2005-04-05 35.279999 477 16828.559523 5.190423 16833.749946
2005-04-06 34.820000 477 16609.140000 5.190423 16614.330423
2005-04-07 34.900002 477 16647.300954 5.190423 16652.491377
2005-04-08 34.599998 477 16504.199046 5.190423 16509.389469
2005-04-11 34.599998 477 16504.199046 5.190423 16509.389469
2005-04-12 34.630001 477 16518.510477 5.190423 16523.700900
2005-04-13 34.299999 477 16361.099523 5.190423 16366.289946
2005-04-14 33.970001 477 16203.690477 5.190423 16208.880900
2005-04-15 33.020000 477 15750.540000 5.190423 15755.730423
2005-04-18 33.000000 477 15741.000000 5.190423 15746.190423
2005-04-19 32.779999 477 15636.059523 5.190423 15641.249946
2005-04-20 33.250000 477 15860.250000 5.190423 15865.440423
2005-04-21 33.730000 477 16089.210000 5.190423 16094.400423
2005-04-22 33.040001 477 15760.080477 5.190423 15765.270900

611 rows × 5 columns

In [15]:
s2pnl.loc['MSFT'] # Performance for Microsoft at Strategy two

Out[15]:
Close Share Position Cash Total
Date
2002-11-18 55.849998 179 9997.149642 2.850358 10000.000000
2002-11-19 54.860001 179 9819.940179 2.850358 9822.790537
2002-11-20 56.619999 179 10134.979821 2.850358 10137.830179
2002-11-21 57.840000 179 10353.360000 2.850358 10356.210358
2002-11-22 58.220001 179 10421.380179 2.850358 10424.230537
2002-11-25 58.230000 179 10423.170000 2.850358 10426.020358
2002-11-26 56.900002 179 10185.100358 2.850358 10187.950716
2002-11-27 58.080002 179 10396.320358 2.850358 10399.170716
2002-11-29 57.680000 179 10324.720000 2.850358 10327.570358
2002-12-02 57.689999 179 10326.509821 2.850358 10329.360179
2002-12-03 56.709999 179 10151.089821 2.850358 10153.940179
2002-12-04 56.540001 179 10120.660179 2.850358 10123.510537
2002-12-05 55.340000 179 9905.860000 2.850358 9908.710358
2002-12-06 55.470001 179 9929.130179 2.850358 9931.980537
2002-12-09 53.529999 179 9581.869821 2.850358 9584.720179
2002-12-10 54.009998 141 7615.409718 2055.230282 9670.640000
2002-12-11 54.660000 141 7707.060000 2055.230282 9762.290282
2002-12-12 54.169998 141 7637.969718 2055.230282 9693.200000
2002-12-13 52.500000 141 7402.500000 2055.230282 9457.730282
2002-12-16 54.480000 141 7681.680000 2055.230282 9736.910282
2002-12-17 54.360001 141 7664.760141 2055.230282 9719.990423
2002-12-18 53.529999 141 7547.729859 2055.230282 9602.960141
2002-12-19 53.110001 141 7488.510141 2055.230282 9543.740423
2002-12-20 53.040001 141 7478.640141 2055.230282 9533.870423
2002-12-23 54.000000 141 7614.000000 2055.230282 9669.230282
2002-12-24 53.820000 141 7588.620000 2055.230282 9643.850282
2002-12-26 53.389999 141 7527.989859 2055.230282 9583.220141
2002-12-27 52.970001 141 7468.770141 2055.230282 9524.000423
2002-12-30 52.750000 141 7437.750000 2055.230282 9492.980282
2002-12-31 51.700001 141 7289.700141 2055.230282 9344.930423
2005-03-11 25.090000 184 4616.560000 10.319954 4626.879954
2005-03-14 25.110001 184 4620.240184 10.319954 4630.560138
2005-03-15 24.910000 184 4583.440000 10.319954 4593.759954
2005-03-16 24.629999 184 4531.919816 10.319954 4542.239770
2005-03-17 24.540001 184 4515.360184 10.319954 4525.680138
2005-03-18 24.309999 184 4473.039816 10.319954 4483.359770
2005-03-21 24.200001 184 4452.800184 10.319954 4463.120138
2005-03-22 23.990000 184 4414.160000 10.319954 4424.479954
2005-03-23 24.180000 146 3530.280000 929.159954 4459.439954
2005-03-24 24.280001 146 3544.880146 929.159954 4474.040100
2005-03-28 24.200001 146 3533.200146 929.159954 4462.360100
2005-03-29 23.920000 146 3492.320000 929.159954 4421.479954
2005-03-30 24.160000 146 3527.360000 929.159954 4456.519954
2005-03-31 24.170000 146 3528.820000 929.159954 4457.979954
2005-04-01 24.120001 184 4438.080184 12.599916 4450.680100
2005-04-04 24.230000 184 4458.320000 12.599916 4470.919916
2005-04-05 24.469999 184 4502.479816 12.599916 4515.079732
2005-04-06 24.670000 184 4539.280000 12.599916 4551.879916
2005-04-07 25.100000 184 4618.400000 12.599916 4630.999916
2005-04-08 24.940001 184 4588.960184 12.599916 4601.560100
2005-04-11 24.969999 184 4594.479816 12.599916 4607.079732
2005-04-12 25.320000 184 4658.880000 12.599916 4671.479916
2005-04-13 25.040001 184 4607.360184 12.599916 4619.960100
2005-04-14 24.840000 184 4570.560000 12.599916 4583.159916
2005-04-15 24.459999 184 4500.639816 12.599916 4513.239732
2005-04-18 24.650000 184 4535.600000 12.599916 4548.199916
2005-04-19 24.629999 184 4531.919816 12.599916 4544.519732
2005-04-20 24.320000 184 4474.880000 12.599916 4487.479916
2005-04-21 25.280001 184 4651.520184 12.599916 4664.120100
2005-04-22 24.980000 184 4596.320000 12.599916 4608.919916

611 rows × 5 columns

###### Get a detailed view of the tickers available in meta output, with only tickers, closing prices and totals.¶

The problem of date span is fixed thanks to Pandas DataReader’s built-in facilities.

In [16]:
pnlAll = {'Close': s1pnl[:, :, 'Close'],
'Total1': s1pnl[:, :, 'Total'],
'Total2': s2pnl[:, :, "Total"]}

pnlAll = pd.Panel(pnlAll).transpose(2, 1, 0)

pnlAll

Out[16]:
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 611 (major_axis) x 3 (minor_axis)
Items axis: AMZN to MSFT
Major_axis axis: 2002-11-18 00:00:00 to 2005-04-22 00:00:00
Minor_axis axis: Close to Total2
In [17]:
pnlAll.keys()

Out[17]:
Index([u'AMZN', u'MSFT'], dtype='object')
In [18]:
pnlAll.loc['AMZN']

Out[18]:
Close Total1 Total2
Date
2002-11-18 22.420000 10000.000000 10000.000000
2002-11-19 21.290001 9496.020446 9496.020446
2002-11-20 22.900000 10214.080000 10214.080000
2002-11-21 23.400000 10214.080000 10379.580000
2002-11-22 23.990000 10214.080000 10574.870000
2002-11-25 24.250000 10214.080000 10660.930000
2002-11-26 23.400000 10214.080000 10379.580000
2002-11-27 24.080000 10214.080000 10604.660000
2002-11-29 23.350000 10214.080000 10363.030000
2002-12-02 24.110001 10214.080000 10614.590331
2002-12-03 23.740000 10057.569577 10451.789891
2002-12-04 23.389999 9909.519154 10297.789451
2002-12-05 22.580000 9566.889577 9941.389891
2002-12-06 22.610001 9579.580000 9954.590331
2002-12-09 21.680000 9579.580000 9628.159980
2002-12-10 21.860001 9579.580000 9691.340331
2002-12-11 22.129999 9579.580000 9691.340331
2002-12-12 22.299999 9579.580000 9691.340331
2002-12-13 22.180000 9579.580000 9691.340331
2002-12-16 22.510000 9579.580000 9691.340331
2002-12-17 22.459999 9579.580000 9691.340331
2002-12-18 22.100000 9579.580000 9691.340331
2002-12-19 21.690001 9579.580000 9691.340331
2002-12-20 21.930000 9579.580000 9691.340331
2002-12-23 22.240000 9579.580000 9691.340331
2002-12-24 21.879999 9579.580000 9691.340331
2002-12-26 20.299999 9579.580000 9691.340331
2002-12-27 18.860001 9579.580000 9691.340331
2002-12-30 19.250000 9579.580000 9691.340331
2002-12-31 18.889999 9579.580000 9691.340331
2005-03-11 34.750000 12784.659562 16662.930233
2005-03-14 34.599998 12729.608828 16591.079275
2005-03-15 34.180000 12575.469562 16389.900233
2005-03-16 33.869999 12461.699195 16241.409754
2005-03-17 33.970001 12498.399929 16289.310712
2005-03-18 34.160000 12568.129562 16380.320233
2005-03-21 33.669998 12388.298828 16145.609275
2005-03-22 33.150002 12197.460296 15896.531191
2005-03-23 33.180000 12208.469562 15910.900233
2005-03-24 32.880001 12208.469562 15796.300615
2005-03-28 33.389999 12208.469562 15991.119851
2005-03-29 33.430000 12208.469562 16006.400233
2005-03-30 34.320000 12208.469562 16346.380233
2005-03-31 34.270000 12208.469562 16327.280233
2005-04-01 34.009998 12208.469562 16227.959469
2005-04-04 35.000000 12562.890278 16700.190423
2005-04-05 35.279999 12663.129920 16833.749946
2005-04-06 34.820000 12498.450278 16614.330423
2005-04-07 34.900002 12527.090994 16652.491377
2005-04-08 34.599998 12419.689562 16509.389469
2005-04-11 34.599998 12419.689562 16509.389469
2005-04-12 34.630001 12430.430636 16523.700900
2005-04-13 34.299999 12312.289920 16366.289946
2005-04-14 33.970001 12194.150636 16208.880900
2005-04-15 33.020000 11854.050278 15755.730423
2005-04-18 33.000000 11846.890278 15746.190423
2005-04-19 32.779999 11768.129920 15641.249946
2005-04-20 33.250000 11936.390278 15865.440423
2005-04-21 33.730000 12108.230278 16094.400423
2005-04-22 33.040001 11861.210636 15765.270900

611 rows × 3 columns

In [19]:
pnlAll.loc['MSFT']

Out[19]:
Close Total1 Total2
Date
2002-11-18 55.849998 10000.000000 10000.000000
2002-11-19 54.860001 9822.790537 9822.790537
2002-11-20 56.619999 10137.830179 10137.830179
2002-11-21 57.840000 10356.210358 10356.210358
2002-11-22 58.220001 10424.230537 10424.230537
2002-11-25 58.230000 10426.020358 10426.020358
2002-11-26 56.900002 10187.950716 10187.950716
2002-11-27 58.080002 10399.170716 10399.170716
2002-11-29 57.680000 10327.570358 10327.570358
2002-12-02 57.689999 10329.360179 10329.360179
2002-12-03 56.709999 10153.940179 10153.940179
2002-12-04 56.540001 10123.510537 10123.510537
2002-12-05 55.340000 9908.710358 9908.710358
2002-12-06 55.470001 9931.980537 9931.980537
2002-12-09 53.529999 9584.720179 9584.720179
2002-12-10 54.009998 9670.640000 9670.640000
2002-12-11 54.660000 9670.640000 9762.290282
2002-12-12 54.169998 9670.640000 9693.200000
2002-12-13 52.500000 9670.640000 9457.730282
2002-12-16 54.480000 9670.640000 9736.910282
2002-12-17 54.360001 9670.640000 9719.990423
2002-12-18 53.529999 9670.640000 9602.960141
2002-12-19 53.110001 9670.640000 9543.740423
2002-12-20 53.040001 9670.640000 9533.870423
2002-12-23 54.000000 9670.640000 9669.230282
2002-12-24 53.820000 9670.640000 9643.850282
2002-12-26 53.389999 9670.640000 9583.220141
2002-12-27 52.970001 9670.640000 9524.000423
2002-12-30 52.750000 9670.640000 9492.980282
2002-12-31 51.700001 9670.640000 9344.930423
2005-03-11 25.090000 4759.749829 4626.879954
2005-03-14 25.110001 4763.530018 4630.560138
2005-03-15 24.910000 4725.729829 4593.759954
2005-03-16 24.629999 4672.809640 4542.239770
2005-03-17 24.540001 4655.800018 4525.680138
2005-03-18 24.309999 4612.329640 4483.359770
2005-03-21 24.200001 4591.540018 4463.120138
2005-03-22 23.990000 4551.849829 4424.479954
2005-03-23 24.180000 4587.759829 4459.439954
2005-03-24 24.280001 4587.759829 4474.040100
2005-03-28 24.200001 4587.759829 4462.360100
2005-03-29 23.920000 4587.759829 4421.479954
2005-03-30 24.160000 4587.759829 4456.519954
2005-03-31 24.170000 4587.759829 4457.979954
2005-04-01 24.120001 4587.759829 4450.680100
2005-04-04 24.230000 4608.659639 4470.919916
2005-04-05 24.469999 4654.259449 4515.079732
2005-04-06 24.670000 4692.259639 4551.879916
2005-04-07 25.100000 4773.959639 4630.999916
2005-04-08 24.940001 4743.559829 4601.560100
2005-04-11 24.969999 4749.259449 4607.079732
2005-04-12 25.320000 4815.759639 4671.479916
2005-04-13 25.040001 4762.559829 4619.960100
2005-04-14 24.840000 4724.559639 4583.159916
2005-04-15 24.459999 4652.359449 4513.239732
2005-04-18 24.650000 4688.459639 4548.199916
2005-04-19 24.629999 4684.659449 4544.519732
2005-04-20 24.320000 4625.759639 4487.479916
2005-04-21 25.280001 4808.159829 4664.120100
2005-04-22 24.980000 4751.159639 4608.919916

611 rows × 3 columns

##### Visualization¶

The codes below visualizes the performance of strategy one and two, with regard to the two stocks being analyzed. For both chart the red line represents strategy one and the blue line represents strategy two.

In [20]:
import matplotlib.pyplot as plt
plt.style.use('ggplot') # Set plotting style

# The plot for Amazon
plt.xlabel('Date')
plt.ylabel('Total amount of money')
plt.title('Results of Amazon, Red = strategy 1 and Blue = strategy 2')
plt.plot(pnlAll.loc['AMZN'].index, pnlAll.loc['AMZN'].Total1, pnlAll.loc['AMZN'].index, pnlAll.loc['AMZN'].Total2)
plt.show()

In [21]:
# The plot for Microsoft
plt.xlabel('Date')
plt.ylabel('Total amount of money')
plt.title('Results of Microsoft, Red = strategy 1 and Blue = strategy 2')
plt.plot(pnlAll.loc['MSFT'].index, pnlAll.loc['MSFT'].Total1, pnlAll.loc['MSFT'].index, pnlAll.loc['MSFT'].Total2)
plt.show()

###### Data for analysis:¶

Extracting time series of monthly returns. Why? Because the updating frequency of each strategy is monthly, so daily return doesn’t really capture the full performance of strategies.

Indexes to look at include:

1. Monthly logarithmic returns
2. Volatility: the standard deviation of monthly logarithmic returns
3. Sharp Ratio: risk/return measures.
4. Max Drop:
5. Benchmark performance:
In [ ]:
### A function to get the end dates of each month
dateRange = [tempTradeDays[0]] # A list with first date
tempYear = None
for yr in dictYears.keys():
tempYear = pd.DatetimeIndex(dictYears[yr]).groupby(pd.DatetimeIndex(dictYears[yr]).month)
for m in tempYear.keys():
dateRange.append(max(tempYear[m]))
dateRange = pd.DatetimeIndex(dateRange).order()
return(dateRange);

import math;

# A function to compute log return
## df is a time-series with datetimeIndex and monthly return
def logReturn(df):
ratio = df/df.shift(1)
log_return = ratio.apply(lambda x: math.log(x))
return(log_return);

monthly_end_date = getMonthDates(pnlAll.iloc[0].index)
returnAll = pnlAll.loc[:,monthly_end_date, :]
returnAll.ix[:,:, 'Log_Return1'] = returnAll.ix[:,:, 'Total1'].apply(logReturn)
returnAll.ix[:,:, 'Log_Return2'] = returnAll.ix[:,:, 'Total2'].apply(logReturn)


The log return for strategy one on Amazon and Microsoft.

In [25]:
returnAll.ix[:,:, 'Log_Return1']

Out[25]:
AMZN MSFT
2002-11-18 NaN NaN
2002-11-29 0.035660 0.032232
2002-12-31 -0.067012 -0.099983
2003-01-31 0.110158 -0.071280
2003-02-28 -0.001302 -0.681804
2003-03-31 0.169363 0.027925
2003-04-30 0.097159 0.054393
2003-05-30 0.223635 -0.041908
2003-06-30 -0.003452 0.040715
2003-07-31 0.099287 0.029568
2003-08-29 0.113712 0.004154
2003-09-30 0.044486 0.047104
2003-10-31 0.116652 -0.055374
2003-11-28 -0.099610 -0.025303
2003-12-31 -0.068959 0.061406
2004-01-30 -0.029324 0.010154
2004-02-27 -0.138676 -0.041250
2004-03-31 -0.008835 -0.058259
2004-04-30 0.001609 0.045516
2004-05-28 0.077932 0.003801
2004-06-30 0.108956 0.084706
2004-07-30 -0.157982 -0.002443
2004-08-31 -0.018121 -0.044570
2004-09-30 0.068388 0.012039
2004-10-29 -0.158930 0.011485
2004-11-30 0.122471 -0.044363
2004-12-31 0.109772 -0.006555
2005-01-31 -0.030923 -0.016569
2005-02-28 -0.187282 -0.043458
2005-03-31 -0.038580 -0.039967
2005-04-22 -0.035028 0.033298

The log return for strategy two on Amazon and Microsoft.

In [26]:
returnAll.ix[:,:, 'Log_Return2']

Out[26]:
AMZN MSFT
2002-11-18 NaN NaN
2002-11-29 0.035660 0.032232
2002-12-31 -0.067012 -0.099983
2003-01-31 0.110158 -0.071280
2003-02-28 -0.001302 -0.681804
2003-03-31 0.169363 0.027925
2003-04-30 0.097159 0.054393
2003-05-30 0.223635 -0.041908
2003-06-30 -0.003452 0.040715
2003-07-31 0.099287 0.029568
2003-08-29 0.113712 0.004154
2003-09-30 0.044486 0.047104
2003-10-31 0.116652 -0.055374
2003-11-28 -0.099610 -0.025303
2003-12-31 -0.068959 0.061406
2004-01-30 -0.029324 0.010154
2004-02-27 -0.138676 -0.041250
2004-03-31 -0.008835 -0.058259
2004-04-30 0.001609 0.045516
2004-05-28 0.077932 0.003801
2004-06-30 0.108956 0.084706
2004-07-30 -0.157982 -0.002443
2004-08-31 -0.018121 -0.044570
2004-09-30 0.068388 0.012039
2004-10-29 -0.158930 0.011485
2004-11-30 0.122471 -0.044363
2004-12-31 0.109772 -0.006555
2005-01-31 -0.030923 -0.016569
2005-02-28 -0.187282 -0.043458
2005-03-31 -0.038580 -0.039967
2005-04-22 -0.035028 0.033298

The volatility for strategy one on the period being tested.

In [30]:
return_volatility_1 = np.std(returnAll.ix[:,:, 'Log_Return1'])
return_volatility_1

Out[30]:
AMZN    0.094982
MSFT    0.128017
dtype: float64

The volatility for strategy two on the period being tested.

In [31]:
return_volatility_2 = np.std(returnAll.ix[:,:, 'Log_Return2'])
return_volatility_2

Out[31]:
AMZN    0.101870
MSFT    0.129399
dtype: float64

The Sharp ratio for strategy one and two. The interest rate for T-Bill at the period is

### One Comment

1. […] temporary final product for these exercises.. Looks funny to put html into a blog post. I am proud of some functions I […]

May 11, 2017