Generating O’Shaughnessy’s Trending Value w/R

This is my code for creating O’Shaughnessy’s Trending Value screen. Not my best code since it was origionally for my personal and private consumption. It uses AAII Stock Investor Pro's exported data. I figured that it might not be bad to share.

Note

I am just an amateur investor sharing my experiences. You should do your own due diligence.

TrendValue.R (Source)

# Trend Value
#https://www.valuesignals.com/Screens/Details/OShaughnessy_Trending_Value

#fix dplr undef13
if(!require("pacman")){
  install.packages("pacman")
}

rm(list = ls(all = TRUE))

# library(dplyr)
# library(quantmod)
# library(xlsx)

pacman::p_load(bindrcpp,dplyr, quantmod, xlsx, rio)

#Momentum will be Regression Relative Strength instead. 126 == 6 month


# 21,42,63,126,189
trenddays <- 126
SIGMA <- 0

ifelse(dir.exists("C:/Users/msghe/OneDrive/Stocks/R"),
       setwd("C:/Users/msghe/OneDrive/Stocks/R"), setwd("C:/Users/michael/SkyDrive/Stocks/R"))


stockdata <-
  read.csv("data/WEEKLY.TXT",
           header = FALSE,
           stringsAsFactors = FALSE,
           na.strings = '-99999999.990')
stocknames <-
  read.csv(
    "data/WEEKLY_KEY.TXT",
    header = FALSE,
    stringsAsFactors = FALSE,
    na.strings = '-99999999.990'
  )
stocknames[, 1]
names(stockdata) <- stocknames[, 1]

#clean ticker name for yahoo finance
stockdata$TICKER <- gsub('.', '-', stockdata$TICKER, fixed = TRUE)
# Remove bad tickers


#NA bad data
# stockdata[stockdata == -99999999.990] <- NA

stockdata <- stockdata[complete.cases(stockdata$TICKER),]

#Create all stock universe


# All stocks is 150 million in  1995 $$. It is adjusted to todays dollors. All
# stocks exclude over the counter.

# min.mark.cap <- quantile(stockdata$MKTCAP, na.rm = TRUE)[[2]]



#find inflation Market Cap 150 @ 1995
getSymbols("CPIAUCSL", src = "FRED")
deflator  <-
  last(Cl(to.yearly(CPIAUCSL)))[[1]] / Cl(to.yearly(CPIAUCSL))['1995'][[1]]

tvminmarketcap <- 150 * deflator

allstock <- stockdata[stockdata$MKTCAP >  tvminmarketcap,]

# stockdata[stockdata$MKTCAP > quantile(stockdata$MKTCAP, na.rm = TRUE)[[4]],]

#Minimum market cap for later sanity


#I will not invest in over the counter
condition <- c("N - New York", "A - American", "M - Nasdaq")
allstock <- filter(allstock, EXCHG_DESC %in% condition)
# min.mark.cap <- median(allstock$MKTCAP, na.rm = TRUE)
# min.mark.cap <- quantile(allstock$MKTCAP, na.rm = TRUE)[[2]]
#add market cap requirement
# allstock <- filter(allstock, MKTCAP > min.mark.cap)
#Minimum Market Cap
min(allstock$MKTCAP)

#Start Ranking the stocks



#Calculate VC2
#subtract ntile from 101 to reverse (correct) Order. So Small is big
allstock$PBVPS.Rank <- 101 - ntile(allstock$PBVPS, 100)
allstock$PE.Rank <- 101 - ntile(allstock$PE, 100)
allstock$PSPS.Rank <- 101 - ntile(allstock$PSPS, 100)
allstock$EVEDA_12M.Rank <- 101 - ntile(allstock$EVEDA_12M, 100)
allstock$PCFPS.Rank <- 101 - ntile(allstock$PCFPS, 100)
allstock$SHY.Rank <- ntile(allstock$SHY, 100)

#Stocks with no rank get 50
allstock$PBVPS.Rank[is.na(allstock$PBVPS.Rank)] <- 50
allstock$PE.Rank[is.na(allstock$PE.Rank)] <- 50
allstock$PSPS.Rank[is.na(allstock$PSPS.Rank)] <- 50
allstock$EVEDA_12M.Rank[is.na(allstock$EVEDA_12M.Rank)] <- 50
allstock$PCFPS.Rank[is.na(allstock$PCFPS.Rank)] <- 50
allstock$SHY.Rank[is.na(allstock$SHY.Rank)] <- 50

#Sum the Ranks
allstock$SumRank <-
  allstock$PBVPS.Rank + allstock$PE.Rank + allstock$PSPS.Rank + allstock$EVEDA_12M.Rank + allstock$PCFPS.Rank + allstock$SHY.Rank

# Calculate VC2
allstock$VC2 <- ntile(allstock$SumRank, 100)

#Get top 10% of allstock

tvstocks <- filter(allstock, VC2 > 70)

#I like Fscore >= 5, Z score >=3
# However, just use Fscore >= 7

#Clean data
tvstocks <- tvstocks[complete.cases(tvstocks$FSCORE_12M),]
# tvstocks <- tvstocks[complete.cases(tvstocks$ZSCORE_Q1),]
#Zscore Prime
# tvstocks <- tvstocks[complete.cases(tvstocks$UDEF13),]

#weed out must sells
# tvstocks <- filter(tvstocks, FSCORE_12M >= 4, ZSCORE_Q1 >= 1.8)
tvstocks <- filter(tvstocks, FSCORE_12M >= 7)
#My Market Cap Needs
# tvstocks <- filter(tvstocks, MKTCAP > min.mark.cap)
# tvstocks <- filter(, YIELD > 0)



#Final Cleaning
# tvstocks <- tvstocks[complete.cases(tvstocks$RPS_6M),]
# tvstocks <- tvstocks[complete.cases(tvstocks$RS_26W),]
# tvstocks <- filter(tvstocks,RS_26W > 0)
# tvstocks <- filter(tvstocks,RS_26W >= median(allstock$PRCHG_26W, na.rm = TRUE))
tvstocks <- filter(tvstocks,PRCHG_13W >= median(allstock$PRCHG_13W, na.rm = TRUE))

# head((screen[order(as.numeric(screen$SCORE), decreasing =  TRUE),]),25) #[1]
# head((screen[order(as.numeric(screen$SCORE), decreasing =  TRUE),]),25) #[1]

# names(stockdata) <- stocknames$V1
# screen.rpt <- left_join(screen, stockdata, by = "TICKER")
# screen.rpt <- arrange(screen.rpt, desc(as.numeric(SCORE)))
# screen.rpt$SCORE <- round(as.numeric(screen.rpt$SCORE), 4)
# # screen.rpt <- arrange(screen.rpt, desc(as.numeric(PRCHG_26W)))
# nrow(screen.rpt)


# stockenv <- new.env()
# getSymbols(tvstocks$TICKER, env = stockenv, adjust=TRUE)
# getSymbols(tvstocks$TICKER, env = stockenv)

# rm(screen)
# Keep it simple, stop using RRS
# tvstocks[,"krs26"] <- NA
# for (i in ls(stockenv)) {
#     print(i)
#     # tvstocks[tvstocks$TICKER == i,"krs26"] <- last(Ad(stockenv[[i]]),1)[[1]]/last(SMA(Ad(stockenv[[i]]),126),1)[[1]]
#     tvstocks[tvstocks$TICKER == i,"krs26"] <- last(Ad(stockenv[[i]]),1)[[1]]/ mean(last(Ad(stockenv[[i]]),126))
# }

screen.rpt <- arrange(tvstocks,desc(as.numeric(PRCHG_26W)))
# screen.rpt <- arrange(tvstocks,desc(as.numeric(RPS_6M)))
# screen.rpt <- arrange(tvstocks,desc(as.numeric(krs26)))

screen.rpt <-
  select(screen.rpt, TICKER, COMPANY, SMG_DESC, PRCHG_26W, MKTCAP,FSCORE_12M,VC2)

head(screen.rpt,25)

write.xlsx(
  head(screen.rpt, 25),
  "MoneyPicks.xlsx",
  sheetName = "Money",
  append = FALSE,
  row.names = FALSE
)

rio::export(head(screen.rpt, 25),"MoneyPicks.csv")

Popular Objects

Out of boredom on a rainy Saturday, I had reconfigured my blog. I kept the s3 bucket, but changed DNS providers and started using CloudFront. One of the reports is called: CloudFront Popular Objects Report. Sad to say, the most popular object had to do with a WordPress live writer exploit.

/xmlrpc.php
/wp2/wp-includes/wlwmanifest.xml
/wp1/wp-includes/wlwmanifest.xml
/wp/wp-includes/wlwmanifest.xml
/wp-includes/wlwmanifest.xml
/wordpress/wp-includes/wlwmanifest.xml
/website/wp-includes/wlwmanifest.xml
/web/wp-includes/wlwmanifest.xml
/test/wp-includes/wlwmanifest.xml
/sito/wp-includes/wlwmanifest.xml
/site/wp-includes/wlwmanifest.xml
/shop/wp-includes/wlwmanifest.xml
/news/wp-includes/wlwmanifest.xml
/media/wp-includes/wlwmanifest.xml
/cms/wp-includes/wlwmanifest.xml
/blog/wp-includes/wlwmanifest.xml
/2018/wp-includes/wlwmanifest.xml
/2017/wp-includes/wlwmanifest.xml
/2016/wp-includes/wlwmanifest.xml
/2015/wp-includes/wlwmanifest.xml

SBYC CHRF Spring 2 on J70 Escape

This Sunday we sailed out for SBYC 2019 CHRF Spring 2. We knew that there was a gale and small craft warning during the race. Santa Barbara forecasts tend to be iffy. You really cannot decide till the time of the race and sometimes afterward.

Issued: 2:32 PM Feb. 16, 2019 – National Weather Service

...GALE WARNING NOW IN EFFECT UNTIL 3 AM PST MONDAY...
...SMALL CRAFT ADVISORY WILL EXPIRE AT 3 PM PST THIS AFTERNOON...

\* Winds...Northwest winds 20 to 30 kt with gusts to 40 kt are

expected when winds are strongest.

\* Seas...Combined seas of 8 to 10 feet with periods around 9
seconds are expected when waves are largest.

SEE THE COASTAL WATERS FORECAST (CWFLOX) FOR MORE DETAILS.

PRECAUTIONARY/PREPAREDNESS ACTIONS...

A Gale Warning means winds of 34 to 47 knots are imminent or
occurring. Operating a vessel in gale conditions requires
experience and properly equipped vessels. It is highly recommended
that mariners without the proper experience seek safe harbor
prior to the onset of gale conditions.

We were a little lite Sunday. Could have used an extra 150 to 200 pounds on the boat. I am still happy that everyone wanted to go out. The call to go in was because I did not feel we could hold the boat down with just the main alone.

Some wind stats from Sunday:

Lighthouse 4 on Sterns Wharf

Summary

Type

Value

Time

#

Wind

HI

24 mph

1:15 PM

288

Wind

LO

2 mph

9:00 PM

288

Wind Gust

HI

34 mph

1:15 PM

288

Wind Gust

LO

7 mph

7:20 AM

288

SB East Buoy

Summary

Type

Value

Time

#

Wind

HI

29 mph

1:50 PM

144

Wind

LO

13 mph

8:20 AM

144

Wind Gust

HI

38 mph

1:40 AM

144

Wind Gust

LO

16 mph

8:40 AM

144

Weather Reports and Race Days

Just a notice to anyone sailing races with me on my J70 Escape. I try to run a safe boat. I rarely call off sailing based on a weather report. I use weather reports more as advisement. The reason is that weather reports in the Santa Barbara area are not that accurate. Also, SBYC always runs a race. The responsibility is on the boat owner. To quote the SI for CHRF:

Immediately prior to any race, the decision to race or not race is the sole responsibility of the boat owner or the owner’s designee. At all times during a race, it is the boat owner’s responsibility to continue or to retire based on unexpected conditions or equipment failure. Boat owners must advise crew members of their responsibility to decide for themselves whether to race or not based on their own competence level

I take this seriously. Example, the first Hot Rum of 2019, I did not fly the kite. I was not confident that it would be safe to sail with it up. I think we were very close to last on a day where j70s screamed with the kite flying.

This is the text I sent to my crew this morning. This verbiage will tend to apply to almost all races:

There is a forecast for heavy wind. Problem with SB is that forecasts are not very accurate. I plan to sail unless actual conditions say otherwise.

I mention this, because I have had crew cancel because of a weather report, and it turned out to be a fun day.

Short Personal Investing History

When I first got into the market was back in the late ’80s. I did not have a lot of money, but I needed some extra income. Back then, you had to buy by the complete block (increments of 100) plus fees where expensive. I found a stock that went up three points and down three points in a very cyclical way every three weeks. I thought I was smart and started buying it long and short. What happened was that one time it went up 10 points when I had shorted. I had learned the dreaded “Margin Call.”

Well, I got out of the market till September 1999. I had learned Mechanical Investing. Problem with Mechanical Investing is that it takes DISCIPLINE. I know I do not have the stomach for the possible 50% drawdown that happens every so often with hot screens.

For a few years, I have done long term buy and hold. I have done well, but have not, in my mind beat the market. Part of the reason is that I work and cannot pay attention to my stocks.

Why this is on my mind, is stock market wealth is really a long term game. People at my level cannot beat the machines nor can make money with friction from taxes or transaction fees. I make money by buying either good stocks or a good basket of stocks. I no longer have the liberty to either gamble or play with mad money.

I am currently looking into Dogs of the Dow, and two screens from “What Works on Wall Street” by James O’Shaughnessy: Trending Value and a version of the Cornerstone Screen. Dogs of the Dow is a good conservative screen with dividends. Trending Value is a tested growth with value screen.

Problem is, there is a part that is back of my mind that states: Just buy $SPY and turn on dividend reinvestment. I will keep up with the market. Not have to worry.

I found an article on R-Bloggers called: Are R^2s Useful In Finance? I am going to use some code from the article. I would have made 5.5% per year. A lot better than what I did, plus head and toes better than what I got in the bank. Don’t ask me what I thought about quantitive easing and wealth transfer from savers.

pacman::p_load(xts,quantmod,PerformanceAnalytics,TTR)


getSymbols('SPY', from = '1999-01-01', src = 'yahoo')
## 'getSymbols' currently uses auto.assign=TRUE by default, but will
## use auto.assign=FALSE in 0.5-0. You will still be able to use
## 'loadSymbols' to automatically load data. getOption("getSymbols.env")
## and getOption("getSymbols.auto.assign") will still be checked for
## alternate defaults.
##
## This message is shown once per session and may be disabled by setting
## options("getSymbols.warning4.0"=FALSE). See ?getSymbols for details.
##
## WARNING: There have been significant changes to Yahoo Finance data.
## Please see the Warning section of '?getSymbols.yahoo' for details.
##
## This message is shown once per session and may be disabled by setting
## options("getSymbols.yahoo.warning"=FALSE).
## [1] "SPY"
adjustedPrices <- Ad(SPY)
monthlyAdj <- to.monthly(adjustedPrices, OHLC=TRUE)

spySMA <- SMA(Cl(monthlyAdj), 10)
spyROC <- ROC(Cl(monthlyAdj), 10)
spyRets <- Return.calculate(Cl(monthlyAdj))

smaRatio <- Cl(monthlyAdj)/spySMA - 1
smaSig <- smaRatio > 0
rocSig <- spyROC > 0

smaRets <- lag(smaSig) * spyRets
rocRets <- lag(rocSig) * spyRets


strats <- na.omit(cbind(smaRets, rocRets, spyRets))
colnames(strats) <- c("SMA10", "MOM10", "BuyHold")
charts.PerformanceSummary(strats, main = "strategies")

image0

rbind(table.AnnualizedReturns(strats), maxDrawdown(strats), CalmarRatio(strats))
## Warning in merge.zoo(fx, .xts(, .index(x))): Index vectors are of different
## classes: integer POSIXct

## Warning in merge.zoo(fx, .xts(, .index(x))): Index vectors are of different
## classes: integer POSIXct

## Warning in merge.zoo(fx, .xts(, .index(x))): Index vectors are of different
## classes: integer POSIXct

## Warning in merge.zoo(fx, .xts(, .index(x))): Index vectors are of different
## classes: integer POSIXct

## Warning in merge.zoo(fx, .xts(, .index(x))): Index vectors are of different
## classes: integer POSIXct

## Warning in merge.zoo(fx, .xts(, .index(x))): Index vectors are of different
## classes: integer POSIXct
##                               SMA10     MOM10   BuyHold
## Annualized Return         0.0719000 0.0776000 0.0559000
## Annualized Std Dev        0.0941000 0.0934000 0.1458000
## Annualized Sharpe (Rf=0%) 0.7646000 0.8312000 0.3835000
## Worst Drawdown            0.1663490 0.1656177 0.5078481
## Calmar Ratio              0.4324006 0.4686707 0.1101006

Hot Rum #3 on Escape

Video of the SBYC 2019 Hot Rum Regatta race #3. Another fun day. There were two other J/70's sailing that day. We traded over the race. It was also a lesson in why the lines should be kept neet since we lost because of an assshole in the spinnaker halyard. We thought it was flaked.

The race had a reverse start. We were going to do a classic start on the outside and sail to the shore. The wind shifted to make the other side of the start line favored.

The wind was shifty, especially close to shore.

The wind was between 10 to 20 knots.

There is a trend of using continuous lines. I am not particularly fond of the continuous spinacker sheet. I think it makes it harder to keep the deck clean. But it is what it is.

SBYC 2019 Hot Rum #1 on Escape

Video of the SBYC 2019 Hot Rum Regatta race #1. All I have to say is we had a lot of fun that day the wind gusts up to 25 miles per hour. We did not fly the spinnaker because I was concerned that we would probably either broach or shrimp the spinnaker. What amazes me was that while we were going out to the starting line, we got up to 9 miles per hour with just the mainsail J/70 that is fast.

I finally figured out how to upload a GoPro video with telemetry, and as one file. The process is not perfect, but doable. The problem is that GoPro saves files as chapters or four-gigabyte files. The normal way is to create a playlist that puts the file together or put the file together with music. For me, that is not acceptable.

In GoPro Quik, open each file in View one at a time. Added the gauges, then create a clip. Save the clip. You have to that with each file. This will make an MP4.

Then use MP4Joiner to merge all the files together. Fun? (not). What I got were an hour and two-minute YouTube video.

2018 in exercise

Just did a report on my garmin connect. It is good to look back at what you did because it puts what you do into perspective of what one has accomplished.

2018 Activities

Activity Type

Total Distance

Average Distance

Max Distance

Average Speed

Cycling

204.79 mi

14.63 mi

28.37 mi

11.1 mph

Running

387.06 mi

2.73 mi

6.81 mi

5.5 mph

Swimming

30.04 mi

0.48 mi

0.93 mi

1.6 mph

Walking

248.13 mi

0.86 mi

4.21 mi

3.1 mph

I did not include "other". Cycling I had just started in late last year. My goals for 2019 most likely will be a lot more than 2018.