Nuts and Bolts of Quantstrat, Part V

This post will be about pre-processing custom indicators in quantstrat–that is, how to add values to your market data that do not arise from the market data itself.

The first four parts of my nuts and bolts of quantstrat were well received. They are even available as a datacamp course. For those that want to catch up to today’s post, I highly recommend the datacamp course.

To motivate this post, the idea is that say you’re using alternative data that isn’t simply derived from a transformation of the market data itself. I.E. you have a proprietary alternative data stream that may predict an asset’s price, you want to employ a cross-sectional ranking system, or any number of things. How do you do this within the context of quantstrat?

The answer is that it’s as simple as binding a new xts to your asset data, as this demonstration will show.

First, let’s get the setup out of the way.

require(quantstrat)
require(PerformanceAnalytics)

initDate="1990-01-01"
from="2003-01-01"
to="2012-12-31"
options(width=70)

options("getSymbols.warning4.0"=FALSE)

currency('USD')
Sys.setenv(TZ="UTC")

symbols <- 'SPY'
suppressMessages(getSymbols(symbols, from=from, to=to, src="yahoo", adjust=TRUE))  

stock(symbols, currency="USD", multiplier=1)

Now, we have our non-derived indicator. In this case, it’s a toy example–the value is 1 if the year is odd (I.E. 2003, 2005, 2007, 2009), and 0 if it’s even. We compute that and simply column-bind (cbind) it to the asset data.

nonDerivedIndicator <- as.numeric(as.character(substr(index(SPY), 1, 4)))%%2 == 1
nonDerivedIndicator <- xts(nonDerivedIndicator, order.by=index(SPY))

SPY <- cbind(SPY, nonDerivedIndicator)
colnames(SPY)[7] = "nonDerivedIndicator"

Next, we just have a very simple strategy–buy a share of SPY on odd years, sell on even years. That is, buy when the nonDerivedIndicator column crosses above 0.5 (from 0 to 1), and sell when the opposite occurs.

strategy.st <- portfolio.st <- account.st <- "nonDerivedData"
rm.strat(strategy.st)
initPortf(portfolio.st, symbols=symbols, initDate=initDate, currency='USD')
initAcct(account.st, portfolios=portfolio.st, initDate=initDate, currency='USD')
initOrders(portfolio.st, initDate=initDate)
strategy(strategy.st, store=TRUE)

add.signal(strategy.st, name = sigThreshold, 
           arguments = list(column = "nonDerivedIndicator", threshold = 0.5, relationship = "gte", cross = TRUE),
           label = "longEntry")

add.signal(strategy.st, name = sigThreshold, 
           arguments = list(column = "nonDerivedIndicator", threshold = 0.5, relationship = "lte", cross = TRUE),
           label = "longExit")


tmp <- applySignals(strategy = strategy.st, mktdata=SPY)


add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open", orderqty = 1), 
         type="enter", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", 
                        ordertype="market", orderside="long", 
                        replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

#apply strategy
t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()
print(t2-t1)

#set up analytics
updatePortf(portfolio.st)
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]
updateAcct(portfolio.st,dateRange)
updateEndEq(account.st)

And the result:

chart.Posn(portfolio.st, 'SPY')

In conclusion, you can create signals based off of any data in quantstrat. Whether that means volatility ratios, fundamental data, cross-sectional ranking, or whatever proprietary alternative data source you may have access to, this very simple process is how you can use quantstrat to add all of those things to your systematic trading backtest research.

Thanks for reading.

Note: I am always interested in full-time opportunities which may benefit from my skills. I have experience in data analytics, asset management, and systematic trading research. If you know of any such opportunities, do not hesitate to contact me on my LinkedIn, found here.

Advertisements

Review: Invoance’s TRAIDE application

This review will be about Inovance Tech’s TRAIDE system. It is an application geared towards letting retail investors apply proprietary machine learning algorithms to assist them in creating systematic trading strategies. Currently, my one-line review is that while I hope the company founders mean well, the application is still in an early stage, and so, should be checked out by potential users/venture capitalists as something with proof of potential, rather than a finished product ready for mass market. While this acts as a review, it’s also my thoughts as to how Inovance Tech can improve its product.

A bit of background: I have spoken several times to some of the company’s founders, who sound like individuals at about my age level (so, fellow millennials). Ultimately, the selling point is this:

Systematic trading is cool.
Machine learning is cool.
Therefore, applying machine learning to systematic trading is awesome! (And a surefire way to make profits, as Renaissance Technologies has shown.)

While this may sound a bit snarky, it’s also, in some ways, true. Machine learning has become the talk of the town, from IBM’s Watson (RenTec itself hired a bunch of speech recognition experts from IBM a couple of decades back), to Stanford’s self-driving car (invented by Sebastian Thrun, who now heads Udacity), to the Netflix prize, to god knows what Andrew Ng is doing with deep learning at Baidu. Considering how well machine learning has done at much more complex tasks than “create a half-decent systematic trading algorithm”, it shouldn’t be too much to ask this powerful field at the intersection of computer science and statistics to help the retail investor glued to watching charts generate a lot more return on his or her investments than through discretionary chart-watching and noise trading. To my understanding from conversations with Inovance Tech’s founders, this is explicitly their mission.

(Note: Dr. Wes Gray and Alpha Architect, in their book DIY Financial Advisor, have already established that listening to pundits, and trying to succeed at discretionary trading, is on a whole, a loser’s game)

However, I am not sure that Inovance’s TRAIDE application actually accomplishes this mission in its current state.

Here’s how it works:

Users select one asset at a time, and select a date range (data going back to Dec. 31, 2009). Assets are currently limited to highly liquid currency pairs, and can take the following settings: 1 hour, 2 hour, 4 hour, 6 hour, or daily bar time frames.

Users then select from a variety of indicators, ranging from technical (moving averages, oscillators, volume calculations, etc. Mostly an assortment of 20th century indicators, though the occasional adaptive moving average has managed to sneak in–namely KAMA–see my DSTrading package, and MAMA–aka the Mesa Adaptive Moving Average, from John Ehlers) to more esoteric ones such as some sentiment indicators. Here’s where things start to head south for me, however. Namely, that while it’s easy to add as many indicators as a user would like, there is basically no documentation on any of them, with no links to reference, etc., so users will have to bear the onus of actually understanding what each and every one of the indicators they select actually does, and whether or not those indicators are useful. The TRAIDE application makes zero effort (thus far) to actually get users acquainted with the purpose of these indicators, what their theoretical objective is (measure conviction in a trend, detect a trend, oscillator type indicator, etc.)

Furthermore, regarding indicator selections, users also specify one parameter setting for each indicator per strategy. E.G. if I had an EMA crossover, I’d have to create a new strategy for a 20/100 crossover, a 21/100 crossover, rather than specifying something like this:

short EMA: 20-60
long EMA: 80-200

Quantstrat itself has this functionality, and while I don’t recall covering parameter robustness checks/optimization (in other words, testing multiple parameter sets–whether one uses them for optimization or robustness is up to the user, not the functionality) in quantstrat on this blog specifically, this information very much exists in what I deem “the official quantstrat manual”, found here. In my opinion, the option of covering a range of values is mandatory so as to demonstrate that any given parameter setting is not a random fluke. Outside of quantstrat, I have demonstrated this methodology in my Hypothesis Driven Development posts, and in coming up for parameter selection for volatility trading.

Where TRAIDE may do something interesting, however, is that after the user specifies his indicators and parameters, its “proprietary machine learning” algorithms (WARNING: COMPLETELY BLACK BOX) determine for what range of values of the indicators in question generated the best results within the backtest, and assign them bullishness and bearishness scores. In other words, “looking backwards, these were the indicator values that did best over the course of the sample”. While there is definite value to exploring the relationships between indicators and future returns, I think that TRAIDE needs to do more in this area, such as reporting P-values, conviction, and so on.

For instance, if you combine enough indicators, your “rule” is a market order that’s simply the intersection of all of the ranges of your indicators. For instance, TRAIDE may tell a user that the strongest bullish signal when the difference of the moving averages is between 1 and 2, the ADX is between 20 and 25, the ATR is between 0.5 and 1, and so on. Each setting the user selects further narrows down the number of trades the simulation makes. In my opinion, there are more ways to explore the interplay of indicators than simply one giant AND statement, such as an “OR” statement, of some sort. (E.G. select all values, put on a trade when 3 out of 5 indicators fall into the selected bullish range in order to place more trades). While it may be wise to filter down trades to very rare instances if trading a massive amount of instruments, such that of several thousand possible instruments, only several are trading at any given time, with TRAIDE, a user selects only *one* asset class (currently, one currency pair) at a time, so I’m hoping to see TRAIDE create more functionality in terms of what constitutes a trading rule.

After the user selects both a long and a short rule (by simply filtering on indicator ranges that TRAIDE’s machine learning algorithms have said are good), TRAIDE turns that into a backtest with a long equity curve, short equity curve, total equity curve, and trade statistics for aggregate, long, and short trades. For instance, in quantstrat, one only receives aggregate trade statistics. Whether long or short, all that matters to quantstrat is whether or not the trade made or lost money. For sophisticated users, it’s trivial enough to turn one set of rules on or off, but TRAIDE does more to hold the user’s hand in that regard.

Lastly, TRAIDE then generates MetaTrader4 code for a user to download.

And that’s the process.

In my opinion, while what Inovance Tech has set out to do with TRAIDE is interesting, I wouldn’t recommend it in its current state. For sophisticated individuals that know how to go through a proper research process, TRAIDE is too stringent in terms of parameter settings (one at a time), pre-coded indicators (its target audience probably can’t program too well), and asset classes (again, one at a time). However, for retail investors, my issue with TRAIDE is this:

There is a whole assortment of undocumented indicators, which then move to black-box machine learning algorithms. The result is that the user has very little understanding of what the underlying algorithms actually do, and why the logic he or she is presented with is the output. While TRAIDE makes it trivially easy to generate any one given trading system, as multiple individuals have stated in slightly different ways before, writing a strategy is the easy part. Doing the work to understand if that strategy actually has an edge is much harder. Namely, checking its robustness, its predictive power, its sensitivity to various regimes, and so on. Given TRAIDE’s rather short data history (2010 onwards), and coupled with the opaqueness that the user operates under, my analogy would be this:

It’s like giving an inexperienced driver the keys to a sports car in a thick fog on a winding road. Nobody disputes that a sports car is awesome. However, the true burden of the work lies in making sure that the user doesn’t wind up smashing into a tree.

Overall, I like the TRAIDE application’s mission, and I think it may have potential as something for the retail investors that don’t intend to learn the ins-and-outs of coding a trading system in R (despite me demonstrating many times over how to put such systems together). I just think that there needs to be more work put into making sure that the results a user sees are indicative of an edge, rather than open the possibility of highly-flexible machine learning algorithms chasing ghosts in one of the noisiest and most dynamic data sets one can possibly find.

My recommendations are these:

1) Multiple asset classes
2) Allow parameter ranges, and cap the number of trials at any given point (E.G. 4 indicators with ten settings each = 10,000 possible trading systems = blow up the servers). To narrow down the number of trial runs, use techniques from experimental design to arrive at decent combinations. (I wish I remembered my response surface methodology techniques from my master’s degree about now!)
3) Allow modifications of order sizing (E.G. volatility targeting, stop losses), such as I wrote about in my hypothesis-driven development posts.
4) Provide *some* sort of documentation for the indicators, even if it’s as simple as a link to investopedia (preferably a lot more).
5) Far more output is necessary, especially for users who don’t program. Namely, to distinguish whether or not there is a legitimate edge, or if there are too few observations to reject the null hypothesis of random noise.
6) Far longer data histories. 2010 onwards just seems too short of a time-frame to be sure of a strategy’s efficacy, at least on daily data (may not be true for hourly).
7) Factor in transaction costs. Trading on an hourly time frame will mean far less P&L per trade than on a daily resolution. If MT4 charges a fixed ticket price, users need to know how this factors into their strategy.
8) Lastly, dogfooding. When I spoke last time with Inovance Tech’s founders, they claimed they were using their own algorithms to create a forex strategy, which was doing well in live trading. By the time more of these suggestions are implemented, it’d be interesting to see if they have a track record as a fund, in addition to as a software provider.

If all of these things are accounted for and automated, the product will hopefully accomplish its mission of bringing systematic trading and machine learning to more people. I think TRAIDE has potential, and I’m hoping that its staff will realize that potential.

Thanks for reading.

NOTE: I am currently contracting in downtown Chicago, and am always interested in networking with professionals in the systematic trading and systematic asset management/allocation spaces. Find my LinkedIn here.

EDIT: Today in my email (Dec. 3, 2015), I received a notice that Inovance was making TRAIDE completely free. Perhaps they want a bunch more feedback on it?

Why Backtesting On Individual Legs In A Spread Is A BAD Idea

So after reading the last post, the author of quantstrat had mostly critical feedback, mostly of the philosophy that prompted its writing in the first place. Basically, the reason I wrote it, as I stated before, is that I’ve seen many retail users of quantstrat constantly ask “how do I model individual spread instruments”, and otherwise try to look like they’re sophisticated by trading spreads.

The truth is that real professionals use industrial-strength tools to determine their intraday hedge ratios (such a tool is called a spreader). The purpose of quantstrat is not to be an execution modeling system, but to be a *strategy* modeling system. Basically, the purpose of your backtest isn’t to look at individual instruments, since in the last post, the aggregate trade statistics told us absolutely nothing about how our actual spread trading strategy performed. The backtest was a mess as far as the analytics were concerned, and thus rendering it more or less useless. So this post, by request of the author of quantstrat, is about how to do the analysis better, and looking at what matters more–the actual performance of the strategy on the actual relationship being traded–namely, the *spread*, rather than the two components.

So, without further ado, let’s look at the revised code:

require(quantmod)
require(quantstrat)
require(IKTrading)

getSymbols("UNG", from="1990-01-01")
getSymbols("DGAZ", from="1990-01-01")
getSymbols("UGAZ", from="1990-01-01")
UNG <- UNG["2012-02-22::"]
UGAZ <- UGAZ["2012-02-22::"]

spread <- 3*OHLC(UNG) - OHLC(UGAZ)

initDate='1990-01-01'
currency('USD')
Sys.setenv(TZ="UTC")
symbols <- c("spread")
stock(symbols, currency="USD", multiplier=1)

strategy.st <- portfolio.st <- account.st <-"spread_strategy_done_better"
rm.strat(portfolio.st)
rm.strat(strategy.st)
initPortf(portfolio.st, symbols=symbols, initDate=initDate, currency='USD')
initAcct(account.st, portfolios=portfolio.st, initDate=initDate, currency='USD')
initOrders(portfolio.st, initDate=initDate)
strategy(strategy.st, store=TRUE)

#### paramters

nEMA = 20

### indicator

add.indicator(strategy.st, name="EMA",
              arguments=list(x=quote(Cl(mktdata)), n=nEMA),
              label="ema")

### signals

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "EMA.ema"), relationship="gt"),
           label="longEntry")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "EMA.ema"), relationship="lt"),
           label="longExit")

### rules

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open", orderqty=1), 
         type="enter", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

#apply strategy
t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()
print(t2-t1)

In this case, things are a LOT simpler. Rather than jumping through the hoops of pre-computing an indicator, along with the shenanigans of separate rules for both the long and the short end, we simply have a spread as it’s theoretically supposed to work–three of an unleveraged ETF against the 3x leveraged ETF, and we can go long the spread, or short the spread. In this case, the dynamic seems to be on the up, and we want to capture that.

So how did we do?

#set up analytics
updatePortf(portfolio.st)
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]
updateAcct(portfolio.st,dateRange)
updateEndEq(account.st)

#trade statistics
tStats <- tradeStats(Portfolios = portfolio.st, use="trades", inclZeroDays=FALSE)
tStats[,4:ncol(tStats)] <- round(tStats[,4:ncol(tStats)], 2)
print(data.frame(t(tStats[,-c(1,2)])))
(aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses))
(aggCorrect <- mean(tStats$Percent.Positive))
(numTrades <- sum(tStats$Num.Trades))
(meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE))

And here’s the output:

> print(data.frame(t(tStats[,-c(1,2)])))
                   spread
Num.Txns            76.00
Num.Trades          38.00
Net.Trading.PL       9.87
Avg.Trade.PL         0.26
Med.Trade.PL        -0.10
Largest.Winner       7.76
Largest.Loser       -1.06
Gross.Profits       21.16
Gross.Losses       -11.29
Std.Dev.Trade.PL     1.68
Percent.Positive    39.47
Percent.Negative    60.53
Profit.Factor        1.87
Avg.Win.Trade        1.41
Med.Win.Trade        0.36
Avg.Losing.Trade    -0.49
Med.Losing.Trade    -0.46
Avg.Daily.PL         0.26
Med.Daily.PL        -0.10
Std.Dev.Daily.PL     1.68
Ann.Sharpe           2.45
Max.Drawdown        -4.02
Profit.To.Max.Draw   2.46
Avg.WinLoss.Ratio    2.87
Med.WinLoss.Ratio    0.78
Max.Equity          13.47
Min.Equity          -1.96
End.Equity           9.87
> (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses))
[1] 1.874225
> (aggCorrect <- mean(tStats$Percent.Positive))
[1] 39.47
> (numTrades <- sum(tStats$Num.Trades))
[1] 38
> (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE))
[1] 2.87

In other words, the typical profile for a trend follower, rather than the uninformative analytics from the last post. Furthermore, the position sizing and equity curve chart actually make sense now. Here they are.

To conclude, while it’s possible to model spreads using individual legs, it makes far more sense in terms of analytics to actually examine the performance of the strategy on the actual relationship being traded, which is the spread itself. Furthermore, after constructing the spread as a synthetic instrument, it can be treated like any other regular instrument in the context of analysis in quantstrat.

Thanks for reading.

NOTE: I am a freelance consultant in quantitative analysis on topics related to this blog. If you have contract or full time roles available for proprietary research that could benefit from my skills, please contact me through my LinkedIn here.

A Way To Model Execution On Individual Legs Of A Spread In Quantstrat

In this post, I’ll attempt to address a question I’ve seen tossed around time and again regarding quantstrat.

“How do I model executions on individual underlying instruments in spread trading?”

First off, a disclaimer: this method is a bit of a kludge, and in using it, you’ll lose out on quantstrat’s inbuilt optimization functionality. Essentially, it builds upon the pre-computed signal methodology I described in a previous post.

Essentially, by appending a column with the same name but with different values to two separate instruments, I can “trick” quantstrat into providing me desired behavior by modeling trading on two underlying instruments.

SO here’s the strategy:

Go long 3 shares of the UNG (natural gas) ETF against 1 share of UGAZ (3x bull) when the spread crosses above its 20-day exponential moving average, otherwise, do nothing. Here’s the reasoning as to why:

require(quantmod)
require(quantstrat)
require(IKTrading)

getSymbols("UNG", from="1990-01-01")
getSymbols("DGAZ", from="1990-01-01")
getSymbols("UGAZ", from="1990-01-01")
UNG <- UNG["2012-02-22::"]
UGAZ <- UGAZ["2012-02-22::"]

spread <- 3*OHLC(UNG) - OHLC(UGAZ)

nEMA=20

chart_Series(spread)
add_TA(EMA(Cl(spread), n=nEMA), on=1, col="blue", lwd=1.5)
legend(x=5, y=50, legend=c("EMA 20"),
       fill=c("blue"), bty="n")

With the corresponding plot:

So, as you can see, we have a spread that drifts upward (something to do with the nature of the leveraged ETF)? So, let’s try and capture that with a strategy.

The way I’m going to do that is to precompute a signal–whether or not the spread’s close is above its EMA20, and append that signal to UNG, with the negative of said signal appended to UGAZ, and then encapsulate it in a quantstrat strategy. In this case, there’s no ATR order sizing function or initial equity–just a simple 3 UNG to 1 UGAZ trade.

signal <- Cl(spread) > EMA(Cl(spread), n=nEMA)
UNG$precomputedSig <- signal
UGAZ$precomputedSig <- signal*-1

initDate='1990-01-01'
currency('USD')
Sys.setenv(TZ="UTC")
symbols <- c("UNG", "UGAZ")
stock(symbols, currency="USD", multiplier=1)

strategy.st <- portfolio.st <- account.st <-"spread_strategy"

rm.strat(portfolio.st)
rm.strat(strategy.st)
initPortf(portfolio.st, symbols=symbols, initDate=initDate, currency='USD')
initAcct(account.st, portfolios=portfolio.st, initDate=initDate, currency='USD')
initOrders(portfolio.st, initDate=initDate)
strategy(strategy.st, store=TRUE)

#long rules
add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputedSig", threshold=.5, 
                          relationship="gt", cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputedSig", threshold=.5, 
                          relationship="lt", cross=TRUE),
           label="longExit")

#short rules
add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputedSig", threshold=-.5, 
                          relationship="lt", cross=TRUE),
           label="shortEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputedSig", threshold=-.5, 
                          relationship="gt", cross=TRUE),
           label="shortExit")

#buy 3
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open", orderqty=3), 
         type="enter", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

#short 1
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="shortEntry", sigval=TRUE, ordertype="market", 
                        orderside="short", replace=FALSE, prefer="Open", orderqty=-1), 
         type="enter", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="shortExit", sigval=TRUE, orderqty="all", ordertype="market", 
                        orderside="short", replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()
print(t2-t1)

#set up analytics
updatePortf(portfolio.st)
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]
updateAcct(portfolio.st,dateRange)
updateEndEq(account.st)

So, did our spread trade work?

#trade statistics
tStats <- tradeStats(Portfolios = portfolio.st, use="trades", inclZeroDays=FALSE)
tStats[,4:ncol(tStats)] <- round(tStats[,4:ncol(tStats)], 2)
print(data.frame(t(tStats[,-c(1,2)])))
(aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses))
(aggCorrect <- mean(tStats$Percent.Positive))
(numTrades <- sum(tStats$Num.Trades))
(meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE))

> (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses))
[1] 1.101303
> (aggCorrect <- mean(tStats$Percent.Positive))
[1] 51.315
> (numTrades <- sum(tStats$Num.Trades))
[1] 76
> (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE))
[1] 1.065

Sort of. However, when you think about it–looking at the statistics on a per-instrument basis in a spread trade is a bit of a red herring. After all, outside of a small spread, what one instrument makes, another will lose, so the aggregate numbers should be only slightly north of 1 or 50% in most cases, which is what we see here.

A better way of looking at whether or not the strategy performs is to look at the cumulative sum of the daily P&L.

#portfolio cash PL
portString <- paste0("portfolio.", portfolio.st)
portPL <- .blotter[[portString]]$summary$Net.Trading.PL
portPL <- portPL[-1,] #remove initialization date
plot(cumsum(portPL))

With the following equity curve:

Is this the greatest equity curve? Probably not. In fact, after playing around with the strategy a little bit, it’s better to actually get in at the close of the next day than the open (apparently there’s some intraday mean-reversion).

Furthermore, one thing to be careful of is that in this backtest, I made sure that for UNG, my precomputedSig would only take values 1 and 0, and vice versa for the UGAZ variant, such that I could write the rules I did. If it took the values 1, 0, and -1, or 1 and -1, the results would not make sense.

In conclusion, the method I showed was essentially a method building on a previous technique of pre-computing signals. Doing this will disallow users to use quantstrat’s built-in optimization functionality, but will allow users to backtest individual leg execution.

To answer one last question, if one wanted to short the spread as well, the thing to do using this methodology would be to pre-compute a second column called, say, precomputedSig2, that behaved the opposite way.

Thanks for reading.

NOTE: I am a freelance consultant in quantitative analysis on topics related to this blog. If you have contract or full time roles available for proprietary research that could benefit from my skills, please contact me through my LinkedIn here.

Predicting High Yield with SPY–a Two Part Post

This post will cover ideas from two individuals: David Varadi of CSS Analytics with whom I am currently collaborating on some volatility trading strategies (the extent of which I hope will end up as a workable trading strategy–my current replica of some of VolatilityMadeSimple’s publicly displayed “example” strategies (note, from other blogs, not to be confused with their proprietary strategy) are something that I think is too risky to be traded as-is), and Cesar Alvarez, of Alvarez Quant Trading. If his name sounds familiar to some of you, that’s because it should. He used to collaborate (still does?) with Larry Connors of TradingMarkets.com, and I’m pretty sure that sometime in the future, I’ll cover those strategies as well.

The strategy for this post is simple, and taken from this post from CSS Analytics.

Pretty straightforward–compute a 20-day SMA on the SPY (I use unadjusted since that’s what the data would have actually been). When the SPY’s close crosses above the 20-day SMA, buy the high-yield bond index, either VWEHX or HYG, and when the converse happens, move to the cash-substitute security, either VFISX or SHY.

Now, while the above paragraph may make it seem that VWEHX and HYG are perfect substitutes, well, they aren’t, as no two instruments are exactly alike, which, as could be noted from my last post, is a detail that one should be mindful of. Even creating a synthetic “equivalent” is never exactly perfect. Even though I try my best to iron out such issues, over the course of generally illustrating an idea, the numbers won’t line up exactly (though hopefully, they’ll be close). In any case, it’s best to leave an asterisk whenever one is forced to use synthetics for the sake of a prolonged backtest.

The other elephant/gorilla in the room (depending on your preference for metaphorical animals), is whether or not to use adjusted data. The upside to that is that dividends are taken into account. The *downside* to that is that the data isn’t the real data, and also assumes a continuous reinvestment of dividends. Unfortunately, shares of a security are not continuous quantities–they are discrete quantities made so by their unit price, so the implicit assumptions in adjusted prices can be optimistic.

For this particular topic, Cesar Alvarez covered it exceptionally well on his blog post, and I highly recommend readers give that post a read, in addition to following his blog in general. However, just to illustrate the effect, let’s jump into the script.


getSymbols("VWEHX", from="1950-01-01")
getSymbols("SPY", from="1900-01-01")
getSymbols("HYG", from="1990-01-01")
getSymbols("SHY", from="1990-01-01")
getSymbols("VFISX", from="1990-01-01")

spySma20Cl <- SMA(Cl(SPY), n=20)
clSig <- Cl(SPY) > spySma20Cl
clSig <- lag(clSig, 1)

vwehxCloseRets <- Return.calculate(Cl(VWEHX))
vfisxCloseRets <- Return.calculate(Cl(VFISX))
vwehxAdjustRets <- Return.calculate(Ad(VWEHX))
vfisxAdjustRets <- Return.calculate(Ad(VFISX))

hygCloseRets <- Return.calculate(Cl(HYG))
shyCloseRets <- Return.calculate(Cl(SHY))
hygAdjustRets <- Return.calculate(Ad(HYG))
shyAdjustRets <- Return.calculate(Ad(SHY))

mutualAdRets <- vwehxAdjustRets*clSig + vfisxAdjustRets*(1-clSig)
mutualClRets <- vwehxCloseRets*clSig + vfisxCloseRets*(1-clSig)

etfAdRets <- hygAdjustRets*clSig + shyAdjustRets*(1-clSig)
etfClRets <- hygCloseRets*clSig + shyCloseRets*(1-clSig)

Here are the results:


mutualFundBacktest <- merge(mutualAdRets, mutualClRets, join='inner')
charts.PerformanceSummary(mutualFundBacktest)
data.frame(t(rbind(Return.annualized(mutualFundBacktest)*100, 
                   maxDrawdown(mutualFundBacktest)*100,
                   SharpeRatio.annualized(mutualFundBacktest))))

Which produces the following equity curves:

As can be seen, the choice to adjust or not can be pretty enormous. Here are the corresponding three statistics:

               Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0..
VWEHX.Adjusted         14.675379       2.954519                        3.979383
VWEHX.Close             7.794086       4.637520                        3.034225

Even without the adjustment, the strategy itself is…very very good, at least from this angle. Let’s look at the ETF variant now.

etfBacktest <- merge(etfAdRets, etfClRets, join='inner')
charts.PerformanceSummary(etfBacktest)
data.frame(t(rbind(Return.annualized(etfBacktest)*100, 
                   maxDrawdown(etfBacktest)*100,
                   SharpeRatio.annualized(etfBacktest))))

The resultant equity curve:

With the corresponding statistics:

             Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0..
HYG.Adjusted         11.546005       6.344801                       1.4674301
HYG.Close             5.530951       9.454754                       0.6840059

Again, another stark difference. Let’s combine all four variants.

fundsAndETFs <- merge(mutualFundBacktest, etfBacktest, join='inner')
charts.PerformanceSummary(fundsAndETFs)
data.frame(t(rbind(Return.annualized(fundsAndETFs)*100, 
                   maxDrawdown(fundsAndETFs)*100,
                   SharpeRatio.annualized(fundsAndETFs))))

The equity curve:

With the resulting statistics:

               Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0..
VWEHX.Adjusted         17.424070       2.787889                       4.7521579
VWEHX.Close            11.739849       3.169040                       3.8715923
HYG.Adjusted           11.546005       6.344801                       1.4674301
HYG.Close               5.530951       9.454754                       0.6840059

In short, while the strategy itself seems strong, the particular similar (but not identical) instruments used to implement the strategy make a large difference. So, when backtesting, make sure to understand what taking liberties with the data means. In this case, by turning two levers, the Sharpe Ratio varied from less than 1 to above 4.

Next, I’d like to demonstrate a little trick in quantstrat. Although plenty of examples of trading strategies only derive indicators (along with signals and rules) from the market data itself, there are also many strategies that incorporate data from outside simply the price action of the particular security at hand. Such examples would be many SPY strategies that incorporate VIX information, or off-instrument signal strategies like this one.

The way to incorporate off-instrument information into quantstrat simply requires understanding what the mktdata object is, which is nothing more than an xts type object. By default, a security may originally have just the OHLCV and open interest columns. Most demos in the public space generally use data only from the instruments themselves. However, it is very much possible to actually pre-compute signals.

Here’s a continuation of the script to demonstrate, with a demo of the unadjusted HYG leg of this trade:

####### BOILERPLATE FROM HERE
require(quantstrat)

currency('USD')
Sys.setenv(TZ="UTC")
symbols <- "HYG"
stock(symbols, currency="USD", multiplier=1)
initDate="1990-01-01"

strategy.st <- portfolio.st <- account.st <- "preCalc"
rm.strat(portfolio.st)
rm.strat(strategy.st)
initPortf(portfolio.st, symbols=symbols, initDate=initDate, currency='USD')
initAcct(account.st, portfolios=portfolio.st, initDate=initDate, currency='USD')
initOrders(portfolio.st, initDate=initDate)
strategy(strategy.st, store=TRUE)
######### TO HERE

clSig <- Cl(SPY) > SMA(Cl(SPY), n=20)

HYG <- merge(HYG, clSig, join='inner')
names(HYG)[7] <- "precomputed_signal"

#no parameters or indicators--we precalculated our signal

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputed_signal", threshold=.5, 
                          relationship="gt", cross=TRUE),
           label="longEntry")


add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputed_signal", threshold=.5, 
                          relationship="lt", cross=TRUE),
           label="longExit")

add.rule(strategy.st, name="ruleSignal",
         arguments=list(sigcol="longEntry", sigval=TRUE, orderqty=1, ordertype="market",
                        orderside="long", replace=FALSE, prefer="Open"),
         type="exit", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

#apply strategy
t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()
print(t2-t1)

#set up analytics
updatePortf(portfolio.st)
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]
updateAcct(portfolio.st,dateRange)
updateEndEq(account.st)

As you can see, no indicators computed from the actual market data, because the strategy used a pre-computed value to work off of. The lowest-hanging fruit of applying this methodology, of course, would be to append the VIX index as an indicator for trading strategies on the SPY.

And here are the results, trading a unit quantity:

> data.frame(round(t(tradeStats(portfolio.st)[-c(1,2)]),2))
                      HYG
Num.Txns           217.00
Num.Trades         106.00
Net.Trading.PL      36.76
Avg.Trade.PL         0.35
Med.Trade.PL         0.01
Largest.Winner       9.83
Largest.Loser       -2.71
Gross.Profits       67.07
Gross.Losses       -29.87
Std.Dev.Trade.PL     1.67
Percent.Positive    50.00
Percent.Negative    50.00
Profit.Factor        2.25
Avg.Win.Trade        1.27
Med.Win.Trade        0.65
Avg.Losing.Trade    -0.56
Med.Losing.Trade    -0.39
Avg.Daily.PL         0.35
Med.Daily.PL         0.01
Std.Dev.Daily.PL     1.67
Ann.Sharpe           3.33
Max.Drawdown        -7.24
Profit.To.Max.Draw   5.08
Avg.WinLoss.Ratio    2.25
Med.WinLoss.Ratio    1.67
Max.Equity          43.78
Min.Equity          -1.88
End.Equity          36.76

And the corresponding position chart:

Lastly, here are the vanguard links for VWEHX and VFISX. Apparently, neither charge a redemption fee. I’m not sure if this means that they can be freely traded in a systematic fashion, however.

In conclusion, hopefully this post showed a potentially viable strategy, understanding the nature of the data you’re working with, and how to pre-compute values in quantstrat.

Thanks for reading.

Note: I am a freelance consultant in quantitative analysis on topics related to this blog. If you have contract or full time roles available for proprietary research that could benefit from my skills, please contact me through my LinkedIn here.

Nuts and Bolts of Quantstrat, Part IV

This post will provide an introduction to the way that rules work in quantstrat. It will detail market orders along with order-sizing functions (limit orders will be saved for a later date). After this post, readers should be able to understand the strategies written in my blog posts, and should be able to write their own. Unlike indicators and signals, rules usually call one function, which is called “ruleSignal” (there is a function that is specifically designed for rebalancing strategies, but it’s possible to do that outside the bounds of quantstrat). For all intents and purposes, this one function handles all rule executions. However, that isn’t to say that rules cannot be customized, as the ruleSignal function has many different arguments that can take in one of several values, though not all permutations will be explored in this post. Let’s take a look at some rules:

#rules
if(atrOrder) {
  
  add.rule(strategy.st, name="ruleSignal", 
           arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                          orderside="long", replace=FALSE, prefer="Open", 
                          osFUN=osDollarATR, tradeSize=tradeSize, 
                          pctATR=pctATR, atrMod="X"), 
           type="enter", path.dep=TRUE)
} else { 
  add.rule(strategy.st, name="ruleSignal", 
           arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                          orderside="long", replace=FALSE, prefer="Open", 
                          osFUN=osMaxDollar, tradeSize=tradeSize, maxSize=tradeSize), 
           type="enter", path.dep=TRUE)
}

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", 
                        ordertype="market", orderside="long", 
                        replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="filterExit", sigval=TRUE, orderqty="all", 
                        ordertype="market", orderside="long", replace=FALSE, 
                        prefer="Open"), 
         type="exit", path.dep=TRUE)

In this case, the first thing to note is that as quantstrat is an R library, it can also incorporate basic programming concepts into the actual strategy formulation. In this case, depending on a meta-parameter (that is, a parameter not found in the argument of any indicator, signal, or rule) called atrOrder (a boolean), I can choose which rule I wish to add to the strategy configuration.

Next, here’s the format for adding a rule:

1) The call to add.rule
2) The name of the strategy (strategy.st)
3) The name of the strategy function (this is usually “ruleSignal”)
4) The arguments to ruleSignal:

a) The signal column (sigCol)
b) the value that signals a trigger (sigVal)
c) the order type (ordertype)
d) the order side (orderside)
e) to replace any other open signal (replace)
f) The order quantity (orderqty) is no order-sizing function is used.
g) the preferred price (prefer, defaults to Close, but as quantstrat is a next-bar system, I use the open)
h) the order sizing function (osFUN)
i) the arguments to the order-sizing function.
j) There are other arguments to different order types, but we’ll focus on market orders for this post.

5) The rule type (type), which will comprise either “enter” or “exit” for most demos
6) The path.dep argument, which is always TRUE
7) (Not shown) the label for the rule. If you’re interested in writing your demos as quickly as possible, these are not necessary if your entry and exit rules are your absolute final points of logic in your backtest. However, if you wish to look at your orders in detail, or use stop-losses/take-profit orders, then the rules need labels, as well.

While most of the logic to adding your basic rule is almost always boilerplate outside the arguments to ruleSignal, it’s the arguments to ruleSignal that allow users to customize rules.

The sigCol argument is a string that has the exact name of the signal column that you wish to use to generate your entries (or exits) from. This is the same string that went into the label argument of your add.signal function calls. In quantstrat, labels effectively act as logical links between indicators, signals, rules, and more.

The sigVal argument is what value to use to trigger rule logic. Since signal output (so far) is comprised of ones (TRUE) and zeroes (FALSE), I set my sigVal to TRUE. It is possible, however, to make a sigSum rule and then allow the sigVal argument to take other values.

The ordertype argument is the order type. For most of my demos that I’ve presented thus far, I’ve mostly used “market” type orders, which are the simplest. Market orders execute at the next bar after receiving the signal. They do not execute on the signal bar, but the bar after the signal bar. On daily data, this might cause some P/L due to gaps, but on intraday data, the open of the next bar should be very similar to the close of current bar. One thing to note is that using monthly data, quantstrat uses current-bar execution.

The orderside argument takes one of two values–“long” or “short”. This separates rule executions into two bins, such that long sells won’t work on short positions and vice versa. It also serves to add clarity and readability to strategy specifications.

The replace argument functions in the following way: if TRUE, it overrides any other signal on the same day. Generally, I avoid ever setting this to true, as order sets (not shown in this post) exist deliberately to control order replacement. However, for some reason, it defaults to TRUE in quantstrat, so make sure to set it to FALSE whenever you write a strategy.

The orderqty argument applies only when there’s no osFUN specified. It can take a flat value (E.G. 1, 2, 100, etc.), or, when the rule type is “exit”, a quantity of “all”, to flatten a position. In all the sell rules I use in my demos, my strategies do not scale out of positions, but merely flatten them out.

The prefer argument exists for specifying what aspect of a bar a trade will get in on. Quantstrat by default executes at the close of the next bar. I set this argument to “Open” instead to minimize the effect of the next bar transaction.

The osFUN specifies the order-sizing function to use. Unlike the functions passed into the name arguments in quantstrat (for indicators, signals, or rules), the osFUN argument is actually a function object (that is, it’s the actual function, rather than its name) that gets passed in as an argument. Furthermore, and this is critical: all arguments *to* the order-sizing function must be passed into the arguments for ruleSignal. They are covered through the ellipsis functionality that most R functions include. The ellipsis means that additional arguments can be passed in, and these additional arguments usually correspond to functions used inside the original function that’s called. This, of course, has the potential to violate the black-box modular programming paradigm by assuming users know the inner-workings of pre-existing code, but it offers additional flexibility in instances such as these. So, to give an example, in my entry rule that uses the osDollarATR order-sizing function, arguments such as pctATR and tradeSize are not arguments to the ruleSignal function, but to the osDollarATR function. Nevertheless, the point to pass them in when constructing a quantstrat strategy is in the arguments to ruleSignal.

If you do not wish to use an osFUN, simply use a flat quantity, such as 100, or if using exit type orders, use “all” to flatten a position.

Moving outside the arguments to ruleSignal, we have several other arguments:

The type argument takes one of several values–but “enter” and “exit” are the most basic. They do exactly as they state. There are other rule types, such as “chain” (for stop-losses), which have their own mechanics, but for now, know that “enter” and “exit” are the two basic rules you need to get off the ground.

The path.dep argument should always be TRUE for the ruleSignal function.

Finally, add.rule also contains a label argument that I do not often use in my demos, as usually, my rules are the last point of my logic. However, if one wants to do deeper strategy analysis using the order book, then using these labels is critical.

After adding rules, you can simply call applyStrategy and run your backtest. Here’s an explanation of how that’s done:

#apply strategy
t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()
print(t2-t1)

#set up analytics
updatePortf(portfolio.st)
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]
updateAcct(portfolio.st,dateRange)
updateEndEq(account.st)

As an explanation, I enclose the applyStrategy call in some code to print how much time the backtest took. Generally, on these twelve years of daily data, a single market may take between several seconds to thirty seconds (if a strategy has hundreds of trades per market).

The next four lines essentially update the objects initialized in order of dependency: first the portfolio, then the account for a given date range (the duration of the backtest), and then compute the end equity.

This concludes the basic nuts and bolts of creating a basic nuts and bolts strategy in quantstrat. On this blog, when I make more use of other features, I’ll dedicate other nuts and bolts sections so that readers can use all of quantstrat’s features more efficiently.

Thanks for reading.

Nuts and Bolts of Quantstrat, Part III

This post will focus on signals in quantstrat.

In comparison to indicators, signals in quantstrat are far more cut-and-dry, as they describe the interaction of indicators with each other–whether that indicator is simply the close price (“Close”), or a computed indicator, there are only so many ways indicators can interact, and the point of signals is to provide the user with a way of describing these relationships–is one greater than another, is the concern only when the cross occurs, does the indicator pass above or below a certain number, etc.

Here’s the code that will provide the example for the demonstration, from the atrDollarComparison strategy:

#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("Close", "sma"), relationship="gt"),
           label="filter")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=buyThresh, 
                          relationship="lt", cross=FALSE),
           label="rsiLtThresh")

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("filter", "rsiLtThresh"), cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=sellThresh,
                          relationship="gt", cross=TRUE),
           label="longExit")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "sma"), relationship="lt"),
           label="filterExit")

Adding signals to a strategy has a very similar format to adding indicators. The structure is very similar:

1) The call to add.signal
2) The name of the strategy (again, strategy.st makes this very simple)
3) The name of the signal function (the majority of which are on display in the preceding block of code)
4) The arguments to said signal function, passed in the same way they are to indicators (that is, arguments=list(args)), but which are far more similar compared to indicators
5) The label for the signal column, which is highly similar to the labeling for indicator columns.

The first two steps are identical to the add.indicator step, except with add.signal instead of add.indicator. This is cut and dry.

Beyond this, all of the signal functions I use are presented above. They are:

sigComparison, sigThreshold, sigAND, and sigCrossover.

The arguments for all four are very similar. They contain some measure of columns, a threshold, a relationship between the first and second column (or between the first column and the threshold), and whether the signal should return TRUE for the entire duration of the relationship being true, or only on the first day, with the cross argument.

Relationships are specified with a two or three character identifier: “gt” stands for greater than (E.G. SMA50 > SMA200), “gte” stands for greater than or equal to, “lt” and “lte” work similarly, and “eq” stands for equal to (which may be useful for certain logic statements such as “stock makes a new seven-day low”, which can be programmed by comparing the close to the running seven-day min, and checking for equality).

Here’s an explanation of all four sig functions:

The sigComparison function compares two columns, and will return TRUE (aka 1) so long as the specified relationship comparing the first column to the second holds. E.G. it will return 1 if you specify SMA50 > SMA200 for every timestamp (aka bar, for those using OHLC data) that the 50-day SMA is greater than the 200-day SMA. The sigComparison function is best used for setting up filters (EG the classic Close > SMA200 formation). This function takes two columns, and a relationship comparing the first to the second columns.

The sigCrossover is identical to the above, except only returns TRUE on the timestamp (bar) that the relationship moves from FALSE to TRUE. E.G. going with the above example, you would only see TRUE the day that the SMA50 first crossed over the SMA200. The sigCrossover is useful for setting up buy or sell orders in trend-following strategies.

The sigThreshold signal is identical to the two above signals (depending on whether cross is TRUE or FALSE), but instead uses a fixed quantity to compare one indicator to, passed in via the threshold argument. For instance, one can create a contrived example of an RSI buy order with a sigCrossover signal with an RSI indicator and an indicator that’s nothing but the same identical buy threshold all the way down, or one can use the sigThreshold function wherever oscillator-type indicators or uniform-value type indicators (E.G. indicators transformed with a percent rank), wherever all such indicators are involved.

Lastly, the sigAND signal function, to be pedantic, can also be called colloquially as sigIntersect. It’s a signal function I wrote (from my IKTrading package) that checks if multiple signals (whether two or more) are true at the same time, and like the sigThreshold function, can be set to either return all times that the condition holds, or the first day only. I wrote sigAND so that users would be able to structurally tie up multiple signals, such as an RSI threshold cross coupled with a moving-average filter. While quantstrat does have a function called sigFormula, it involves quoted code evaluation, which I wish to minimize as much as possible. Furthermore, using sigAND allows users to escalate the cross clause, meaning that the signals that are used as columns can be written as comparisons, rather than as crosses. E.G. in this RSI 20/80 filtered on SMA200 strategy, I can simply compare if the RSI is less than 20, and only generate a buy rule at the timestamp after both RSI is less than 20 AND the close is greater than its SMA200. It doesn’t matter whether the close is above SMA200 and the RSI crosses under 20, or if the RSI was under 20, and the close crossed above its SMA200. Either combination will trigger the signal.

One thing to note regarding columns passed as arguments to the signals: quantstrat will do its best to “take an educated guess” regarding which column the user attempts to refer to. For instance, when using daily data, the format may often be along the lines of XYZ.Open XYZ.High XYZ.Low XYZ.Close, so when “Close” is one of the arguments, quantstrat will make its best guess that the user means the XYZ.Close column. This is also, why, once again, I stress that reserved keywords (OHLC keywords, analogous tick data keywords) should not be used in labeling. Furthermore, unlike indicators, whose output will usually be something along the lines of FUNCTION_NAME.userLabel, labels for signals are as-is, so what one passes into the label argument is what one gets.

To put it together, here is the chunk of code again, and the English description of what the signals in the chunk of code do:

#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("Close", "sma"), relationship="gt"),
           label="filter")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=buyThresh, 
                          relationship="lt", cross=FALSE),
           label="rsiLtThresh")

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("filter", "rsiLtThresh"), cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=sellThresh,
                          relationship="gt", cross=TRUE),
           label="longExit")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "sma"), relationship="lt"),
           label="filterExit")

1) The first signal checks to see if the “Close” column is greater than (“gt”) the “sma” column (which had a setting of 200), and is labeled “filter”.
2) The second signal checks to see if the “rsi” column is less than (“lt”) the threshold of buyThresh (which was defined earlier as 20), and is labeled as “rsiLtThresh”.
3) The third signal checks when both of the above signals became TRUE for the first time, until one or the other condition becomes false, and is labeled as “longEntry”. NB: the signals themselves do not place the order–I just like to use the label “longEntry” as this allows code in the rules logic to be reused quicker.
4) The fourth signal checks if the “rsi” column crossed over the sell threshold (80), and is labeled as “longExit”.
5) The fifth signal checks if the “Close” column crossed under the “sma” column, and is labeled “filterExit”.

In quantstrat, it’s quite feasible to have multiple signals generate entry orders, and multiple signals generate exit orders. However, make sure that the labels are unique.

The next post will cover rules.

Thanks for reading.