VCI — The Value Charts Indicator
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
So recently, I was made known of the Value Charts Indicator , which was supposed to be some form of alternative to the RSI. I decided to investigate it, and see if it’s worth using.
Before diving into a strategy, here’s how the indicator works:
"VCI" <- function(OHLC, nLookback=40, nRange=8, pctRank=FALSE) { if(nLookback > 7) { varA <- runMax(Hi(OHLC), nRange) - runMin(Lo(OHLC), nRange) varB <- lag(varA, nRange+1) varC <- lag(varA, nRange*2) varD <- lag(varA, nRange*3) varE <- lag(varA, nRange*4) LRange <- (varA+varB+varC+varD+varE)/25 } if(nLookback <=7) { absDiff <- abs(diff(Cl(OHLC))) dailyRange <- Hi(OHLC) - Lo(OHLC) tmp <- cbind(absDiff, dailyRange) maxTmp <- pmax(tmp) LRange <- SMA(maxTmp, 5)*.16 } hilo <- (Hi(OHLC)+Lo(OHLC))/2 VO <- (Op(OHLC)-SMA(hilo, nLookback))/LRange VH <- (Hi(OHLC)-SMA(hilo, nLookback))/LRange VL <- (Lo(OHLC)-SMA(hilo, nLookback))/LRange VC <- (Cl(OHLC)-SMA(hilo, nLookback))/LRange out <- cbind(VO=VO, VH=VH, VL=VL, VC=VC) colnames(out) <- c("VO", "VH", "VL", "VC") return(out) }
Long story short, if the lookback period is 8 bars or more, it is something akin to an average of various five lagged ranges, over five times the specified range. That is, define your first range computation as the difference between the highest high and lowest low, and then average that with that same computation lagged by nRange+1 bars, nRange*2 bars, and so on. At a shorter frame than 8 bars (that is, a special case), the computation is a moving average of the daily maximum between the daily range and the close-to-close range (E.G. with a high of 4 and low of 2, with close of 3 and previous close of 2, that daily value will be equal to 4-2=2), and then take a 5 period SMA of that, and multiply by .16. Although the initial indicator had the range dependent on the lookback period, I chose to decouple it for greater flexibility to the user.
This range calculation is then used as a denominator of a computation that is the difference of the current price minus the SMA value of the average of an (H+L)/2 price proxy. In short, it’s a variation on a theme of the classical z-score from statistics. In other words, (X-Xbar)/(normalizing value).
This z-score is computed for all four price strands.
In my current implementation, I have not yet implemented the functionality for zero-movement bars (though that can be done by request) if anyone sees value with this indicator.
To put this indicator through its paces, I threw about as plain-standard-vanilla strategy around it. The strategy activates upon the close price greater than SMA200 (the “conventional wisdom”), and buys when the indicator crosses under -2 and exits above 2, using a lookback period of 10 days, with a range period of 2 days (the settings the indicator creator(s) had in mind were that -4/+4 was relatively oversold/overbought, with -8/+8 being extremely so). The idea here was to get a bunch of relatively short-term trades going, and use the advantage of large numbers to see how well this indicator performs.
Here’s the strategy code:
require(IKTrading) require(quantstrat) require(PerformanceAnalytics) initDate="1990-01-01" from="2003-01-01" to=as.character(Sys.Date()) options(width=70) source("demoData.R") #trade sizing and initial equity settings tradeSize <- 100000 initEq <- tradeSize*length(symbols) strategy.st <- portfolio.st <- account.st <- "VCI_test" 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',initEq=initEq) initOrders(portfolio.st, initDate=initDate) strategy(strategy.st, store=TRUE) #parameters pctATR=.02 period=10 nRange=2 nLookback=10 pctRank=FALSE buyThresh=-2 sellThresh=2 nSMA=200 #indicators add.indicator(strategy.st, name="lagATR", arguments=list(HLC=quote(HLC(mktdata)), n=period), label="atrX") add.indicator(strategy.st, name="VCI", arguments=list(OHLC=quote(OHLC(mktdata)), nLookback=nLookback, nRange=nRange, pctRank=pctRank), label="vci") add.indicator(strategy.st, name="SMA", arguments=list(x=quote(Cl(mktdata)), n=nSMA), label="sma") #signals add.signal(strategy.st, name="sigComparison", arguments=list(columns=c("Close", "SMA.sma"), relationship="gt"), label="filter") add.signal(strategy.st, name="sigThreshold", arguments=list(column="VC.vci", threshold=buyThresh, relationship="lt", cross=FALSE), label="VCIltThresh") add.signal(strategy.st, name="sigAND", arguments=list(columns=c("filter", "VCIltThresh"), cross=TRUE), label="longEntry") add.signal(strategy.st, name="sigThreshold", arguments=list(column="VC.vci", threshold=sellThresh, relationship="gt", cross=TRUE), label="longExit") add.signal(strategy.st, name="sigCrossover", arguments=list(columns=c("Close", "SMA.sma"), relationship="lt"), label="filterExit") #rules 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) 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) #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 here are the results:
> (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses)) [1] 1.684617 > (aggCorrect <- mean(tStats$Percent.Positive)) [1] 69.466 > (numTrades <- sum(tStats$Num.Trades)) [1] 2801 > (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE)) [1] 0.753 > print(t(durStats)) [,1] Min 1 Q1 5 Med 9 Mean 11 Q3 14 Max 57 WMin 1 WQ1 5 WMed 8 WMean 9 WQ3 12 WMax 41 LMin 1 LQ1 5 LMed 15 LMean 15 LQ3 22 LMax 57 > SharpeRatio.annualized(portfRets) [,1] Annualized Sharpe Ratio (Rf=0%) 0.8951308 > Return.annualized(portfRets) [,1] Annualized Return 0.06821319 > maxDrawdown(portfRets) [1] 0.108064 > round(apply.yearly(dailyRetComparison, Return.cumulative),3) strategy SPY 2003-12-31 0.058 0.066 2004-12-31 0.056 0.079 2005-12-30 0.034 0.025 2006-12-29 0.148 0.132 2007-12-31 0.094 0.019 2008-12-31 -0.022 -0.433 2009-12-31 0.149 0.192 2010-12-31 -0.055 0.110 2011-12-30 0.072 -0.028 2012-12-31 0.072 0.126 2013-12-31 0.057 0.289 2014-08-22 0.143 0.075 > round(apply.yearly(dailyRetComparison, SharpeRatio.annualized),3) strategy SPY 2003-12-31 2.379 3.641 2004-12-31 0.751 0.706 2005-12-30 0.476 0.238 2006-12-29 2.083 1.312 2007-12-31 0.909 0.123 2008-12-31 -0.943 -1.050 2009-12-31 2.023 0.719 2010-12-31 -0.548 0.614 2011-12-30 0.854 -0.122 2012-12-31 1.015 0.990 2013-12-31 0.655 2.594 2014-08-22 2.869 1.137 > round(apply.yearly(dailyRetComparison, maxDrawdown),3) strategy SPY 2003-12-31 0.014 0.025 2004-12-31 0.079 0.085 2005-12-30 0.058 0.074 2006-12-29 0.068 0.077 2007-12-31 0.073 0.102 2008-12-31 0.029 0.520 2009-12-31 0.041 0.280 2010-12-31 0.108 0.167 2011-12-30 0.052 0.207 2012-12-31 0.043 0.099 2013-12-31 0.072 0.062 2014-08-22 0.047 0.058
In short, it has the statistical profile of a standard mean-reverting strategy–lots of winners, losers slightly larger than winners, losers last longer in the market than winners as well. In terms of Sharpe Ratio, it’s solid but not exactly stellar. Overall, the strategy generally sports much better risk control than the raw SPY, but the annualized return to drawdown ratio isn’t quite up to the same level as some strategies tested on this blog in the past.
This is the equity curve comparison.
The equity profile seems to be pretty standard fare–winners happen over time, but a drawdown can wipe some of them (but not all) pretty quickly, as the system continues to make new equity highs. Solid, but not stellar.
Here’s an example of the equity curve of an individual instrument (the usual XLB):
Something to note is that the indicator is fairly choppy, and does best in a strong uptrend, when terms like oversold, pullback, and so on, are actually that, as opposed to a change in trend, or a protracted cyclic downtrend in a sideways market.
Here’s a picture of the strategy on XLB in 2012.
As you can see, the indicator at the 2-bar range isn’t exactly the smoothest, but with proper position-sizing rules (I use position sizing based on a 10-day ATR), the disadvantage of chopping across a threshold can be largely mitigated.
OVerall, while this indicator doesn’t seem to be much better than the more conventional RSIs, it nevertheless seems to be an alternative, and for those that want to use it, it’s now in my IKTrading package.
Thanks for reading.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.