Volatility Histeresis: A First Attempt

[This article was first published on QuantStrat TradeR » R, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

So the last time that a FRAMA strategy was tried with price crossovers, the problem was that due to counter-trending failures, the filter that was added missed a lot of good trades, and wound up losing a lot of money during flat markets that passed the arbitrary filter.

This trading system tries to rectify those issues by trading a rising FRAMA filtered on a 5-day standard deviation ratio.

The hypothesis is this: the FRAMA rises in legitimately trending markets, and stays flat in choppy markets. Therefore, the ratio of standard deviations (that is, a running standard deviation of the FRAMA over the standard deviation of the market close) should be higher during trending markets, and lower during choppy markets. Additionally, as this ratio bottoms out at zero and usually tops out at 1 (rarely gets higher), it can be used as an indicator across instruments of vastly different properties.

The data that will be used will be the quandl futures data file (without federal funds, coffee, or sugar, because of data issues).

Here’s the data file:

require(IKTrading)


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


t1 <- Sys.time()
if(!"CME_CL" %in% ls()) {
  #Energies
  CME_CL <- quandClean("CHRIS/CME_CL", start_date=from, end_date=to, verbose=verbose) #Crude
  CME_NG <- quandClean("CHRIS/CME_NG", start_date=from, end_date=to, verbose=verbose) #NatGas
  CME_HO <- quandClean("CHRIS/CME_HO", start_date=from, end_date=to, verbose=verbose) #HeatingOil
  CME_RB <- quandClean("CHRIS/CME_RB", start_date=from, end_date=to, verbose=verbose) #Gasoline
  ICE_B <- quandClean("CHRIS/ICE_B", start_date=from, end_date=to, verbose=verbose) #Brent
  ICE_G <- quandClean("CHRIS/ICE_G", start_date=from, end_date=to, verbose=verbose) #Gasoil
  
  #Grains
  CME_C <- quandClean("CHRIS/CME_C", start_date=from, end_date=to, verbose=verbose) #Chicago Corn
  CME_S <- quandClean("CHRIS/CME_S", start_date=from, end_date=to, verbose=verbose) #Chicago Soybeans
  CME_W <- quandClean("CHRIS/CME_W", start_date=from, end_date=to, verbose=verbose) #Chicago Wheat
  CME_SM <- quandClean("CHRIS/CME_SM", start_date=from, end_date=to, verbose=verbose) #Chicago Soybean Meal
  CME_KW <- quandClean("CHRIS/CME_KW", start_date=from, end_date=to, verbose=verbose) #Kansas City Wheat
  CME_BO <- quandClean("CHRIS/CME_BO", start_date=from, end_date=to, verbose=verbose) #Chicago Soybean Oil
  
  #Softs
  #ICE_SB <- quandClean("CHRIS/ICE_SB", start_date=from, end_date=to, verbose=verbose) #Sugar
  #Sugar 2007-03-26 is wrong
  #ICE_KC <- quandClean("CHRIS/ICE_KC", start_date=from, end_date=to, verbose=verbose) #Coffee
  #Coffee January of 08 is FUBAR'd
  ICE_CC <- quandClean("CHRIS/ICE_CC", start_date=from, end_date=to, verbose=verbose) #Cocoa
  ICE_CT <- quandClean("CHRIS/ICE_CT", start_date=from, end_date=to, verbose=verbose) #Cotton
  
  #Other Ags
  CME_LC <- quandClean("CHRIS/CME_LC", start_date=from, end_date=to, verbose=verbose) #Live Cattle
  CME_LN <- quandClean("CHRIS/CME_LN", start_date=from, end_date=to, verbose=verbose) #Lean Hogs
  
  #Precious Metals
  CME_GC <- quandClean("CHRIS/CME_GC", start_date=from, end_date=to, verbose=verbose) #Gold
  CME_SI <- quandClean("CHRIS/CME_SI", start_date=from, end_date=to, verbose=verbose) #Silver
  CME_PL <- quandClean("CHRIS/CME_PL", start_date=from, end_date=to, verbose=verbose) #Platinum
  CME_PA <- quandClean("CHRIS/CME_PA", start_date=from, end_date=to, verbose=verbose) #Palladium
  
  #Base
  CME_HG <- quandClean("CHRIS/CME_HG", start_date=from, end_date=to, verbose=verbose) #Copper
  
  #Currencies
  CME_AD <- quandClean("CHRIS/CME_AD", start_date=from, end_date=to, verbose=verbose) #Ozzie
  CME_CD <- quandClean("CHRIS/CME_CD", start_date=from, end_date=to, verbose=verbose) #Loonie
  CME_SF <- quandClean("CHRIS/CME_SF", start_date=from, end_date=to, verbose=verbose) #Franc
  CME_EC <- quandClean("CHRIS/CME_EC", start_date=from, end_date=to, verbose=verbose) #Euro
  CME_BP <- quandClean("CHRIS/CME_BP", start_date=from, end_date=to, verbose=verbose) #Cable
  CME_JY <- quandClean("CHRIS/CME_JY", start_date=from, end_date=to, verbose=verbose) #Yen
  CME_NE <- quandClean("CHRIS/CME_NE", start_date=from, end_date=to, verbose=verbose) #Kiwi
  
  #Equities
  CME_ES <- quandClean("CHRIS/CME_ES", start_date=from, end_date=to, verbose=verbose) #Emini
  CME_MD <- quandClean("CHRIS/CME_MD", start_date=from, end_date=to, verbose=verbose) #Midcap 400
  CME_NQ <- quandClean("CHRIS/CME_NQ", start_date=from, end_date=to, verbose=verbose) #Nasdaq 100
  CME_TF <- quandClean("CHRIS/CME_TF", start_date=from, end_date=to, verbose=verbose) #Russell Smallcap
  CME_NK <- quandClean("CHRIS/CME_NK", start_date=from, end_date=to, verbose=verbose) #Nikkei
  
  #Dollar Index and Bonds/Rates
  ICE_DX  <- quandClean("CHRIS/CME_DX", start_date=from, end_date=to, verbose=verbose) #Dixie
  #CME_FF  <- quandClean("CHRIS/CME_FF", start_date=from, end_date=to, verbose=verbose) #30-day fed funds
  CME_ED  <- quandClean("CHRIS/CME_ED", start_date=from, end_date=to, verbose=verbose) #3 Mo. Eurodollar/TED Spread
  CME_FV  <- quandClean("CHRIS/CME_FV", start_date=from, end_date=to, verbose=verbose) #Five Year TNote
  CME_TY  <- quandClean("CHRIS/CME_TY", start_date=from, end_date=to, verbose=verbose) #Ten Year Note
  CME_US  <- quandClean("CHRIS/CME_US", start_date=from, end_date=to, verbose=verbose) #30 year bond
}

CMEinsts <- c("CL", "NG", "HO", "RB", "C", "S", "W", "SM", "KW", "BO", "LC", "LN", "GC", "SI", "PL", 
              "PA", "HG", "AD", "CD", "SF", "EC", "BP", "JY", "NE", "ES", "MD", "NQ", "TF", "NK", #"FF",
              "ED", "FV", "TY", "US")

ICEinsts <- c("B", "G", #"SB", #"KC", 
              "CC", "CT", "DX")
CME <- paste("CME", CMEinsts, sep="_")
ICE <- paste("ICE", ICEinsts, sep="_")
symbols <- c(CME, ICE)
stock(symbols, currency="USD", multiplier=1)
t2 <- Sys.time()
print(t2-t1)

Here’s the strategy:

require(DSTrading)
require(IKTrading)
require(quantstrat)
require(PerformanceAnalytics)

initDate="1990-01-01"
from="2000-03-01"
to="2011-12-31"
options(width=70)
verose=TRUE

FRAMAsdr <- function(HLC, n, FC, SC, nSD, ...) {
  frama <- FRAMA(HLC, n=n, FC=FC, SC=SC, ...)
  sdr <- runSD(frama$FRAMA, n=nSD)/runSD(Cl(HLC), n=nSD)
  sdr[sdr > 2]  <- 2
  out <- cbind(FRAMA=frama$FRAMA, trigger=frama$trigger, sdr=sdr)
  out
}

source("futuresData.R")

#trade sizing and initial equity settings
tradeSize <- 100000
initEq <- tradeSize*length(symbols)

strategy.st <- portfolio.st <- account.st <- "FRAMA_SDR_I"
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
FC = 1
SC = 300
n = 126
triggerLag = 1
nSD = 5
sdThresh <- .3

period=10
pctATR=.02

#indicators
add.indicator(strategy.st, name="FRAMAsdr",
              arguments=list(HLC=quote(HLC(mktdata)), FC=FC, SC=SC, 
                             n=n, triggerLag=triggerLag, nSD=nSD),
              label="SDR")

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

#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("FRAMA.SDR", "trigger.SDR"), relationship="gt"),
           label="FRAMAup")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="sdr.SDR", threshold=sdThresh, 
                          relationship="gt",cross=FALSE),
           label="SDRgtThresh")

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

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("FRAMA.SDR", "trigger.SDR"), relationship="lt"),
           label="FRAMAdnExit")

#add.signal(strategy.st, name="sigThreshold",
#           arguments=list(column="sdr.SDR", threshold=sdThresh, relationship="lt", cross=TRUE),
#           label="SDRexit")

#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="FRAMAdnExit", 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="SDRexit", 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)

Notice that the exit due to the volatility filter had to have been commented out (as it caused the strategy to lose all its edge). In any case, the FRAMA is the usual 126 day FRAMA, and the running standard deviation is 5 days, in order to try and reduce lag. The standard deviation ratio threshold will be .2 or higher. Here are the results:

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

In other words, typical trend follower results. 40/60 wrong to right, with a 2:1 win to loss ratio. Far from spectacular.

Duration statistics:

print(t(durStats))
      [,1]
Min      1
Q1       2
Med      5
Mean    11
Q3      11
Max    158
WMin     1
WQ1      5
WMed    10
WMean   18
WQ3     21
WMax   158
LMin     1
LQ1      1
LMed     3
LMean    6
LQ3      6
LMax    93

In short, winners last longer than losers, which makes sense given that there are a lot of whipsaws, and that this is a trend-following strategy.

Market exposure:

> print(mean(as.numeric(as.character(mktExposure$MktExposure))))
[1] 0.3820789

38%. So how did it perform?

Like this. Not particularly great, considering it’s a 60% gain over 11 years. Here are the particular statistics:

> SharpeRatio.annualized(portfRets)
                                     [,1]
Annualized Sharpe Ratio (Rf=0%) 0.8124137
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.04229355
> maxDrawdown(portfRets)
[1] 0.07784351

In other words, about 10 basis points of returns per percent of market exposure, or a 10% annualized return. The problem being? The drawdown is much higher than the annualized return, meaning that leverage will only make things worse. Basically, for the low return on exposure and high drawdown to annualized return, this strategy is a failure. While the steadily ascending equity curve is good, it is meaningless when the worst losses take more than a year to recover from.

In any case, here’s a look at some individual instruments.

Here’s the equity curve for the E-minis.

So first off, we can see one little feature of this strategy–due to the entry and exit not being symmetric (that is, it takes two conditions to enter–a rising FRAMA and a standard deviation ratio above .2–and only exits on one of them (falling FRAMA), price action that exhibits a steady grind upwards, due to the rapid change in ATR (it’s a 10-day figure) can actually slightly pyramid from time to time. This is a good feature in my opinion, since it can add onto a winning position. However, in times of extreme volatility, when even an adaptive indicator can get bounced around chasing “mini-trends”, we can see losses pile on.

Next, let’s look at a much worse situation. Here’s the equity curve for the Eurodollar/TED spread.

In this case, it’s clearly visible that the strategy has countertrend issues, as well as the fact that the 5-day standard deviation ratio can be relatively myopic when it comes to instruments that have protracted periods of complete inactivity–that is, the market is not even choppy so much as just still.

I’ll leave this here, and move onto other attempts at getting around this sort of roadblock next.

Thanks for reading.


To leave a comment for the author, please follow the link and comment on their blog: QuantStrat TradeR » R.

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.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)