VCI — The Value Charts Indicator

August 23, 2014

(This article was first published on QuantStrat TradeR » R, and kindly contributed to R-bloggers)

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")

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:




#trade sizing and initial equity settings
tradeSize <- 100000
initEq <- tradeSize*length(symbols) <- <- <- "VCI_test"
initPortf(, symbols=symbols, initDate=initDate, currency='USD')
initAcct(,, initDate=initDate, currency='USD',initEq=initEq)
initOrders(, initDate=initDate)
strategy(, store=TRUE)





add.indicator(, name="lagATR", 
              arguments=list(HLC=quote(HLC(mktdata)), n=period), 

add.indicator(, name="VCI",
              arguments=list(OHLC=quote(OHLC(mktdata)), nLookback=nLookback,
                             nRange=nRange, pctRank=pctRank),

add.indicator(, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), n=nSMA),

add.signal(, name="sigComparison",
           arguments=list(columns=c("Close", "SMA.sma"), relationship="gt"),

add.signal(, name="sigThreshold",
           arguments=list(column="VC.vci", threshold=buyThresh, 
                          relationship="lt", cross=FALSE),

add.signal(, name="sigAND",
           arguments=list(columns=c("filter", "VCIltThresh"), cross=TRUE),

add.signal(, name="sigThreshold",
           arguments=list(column="VC.vci", threshold=sellThresh,
                          relationship="gt", cross=TRUE),

add.signal(, name="sigCrossover",
           arguments=list(columns=c("Close", "SMA.sma"), relationship="lt"),

add.rule(, 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(, 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(, 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(,
t2 <- Sys.time()

#set up analytics
dateRange <- time(getPortfolio($summary)[-1]

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))
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)
Annualized Sharpe Ratio (Rf=0%) 0.8951308
> Return.annualized(portfRets)
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.

To leave a comment for the author, please follow the link and comment on his blog: QuantStrat TradeR » R. offers daily e-mail updates about R news and tutorials on topics such as: visualization (ggplot2, Boxplots, maps, animation), programming (RStudio, Sweave, LaTeX, SQL, Eclipse, git, hadoop, Web Scraping) statistics (regression, PCA, time series, trading) and more...

If you got this far, why not subscribe for updates from the site? Choose your flavor: e-mail, twitter, RSS, or facebook...

Comments are closed.