braverock / quantstrat

289 stars 114 forks source link

Add demo showing code of price action indicators & signals #138

Open climbthemt opened 3 years ago

climbthemt commented 3 years ago

Description

There are no known examples of coding price actions for quantstrat. After days of hunting, I've found nothing on the web, the presentations, or PDFs. The commercial software, AmiBroker, can do it easily in AFL. But quantstrat doesn't seem to support it easily.

An example, create a signal for an ETF, say, SPY. If SPY closes down 3 days in a row then buy. Then sell if SPY closes above the previous high.

I've done some awful kludgy coding to try to do this, but was unsuccessful. If someone knows how to code that, I can put it into a Luxor example format and submit it. However, I don't know how to solve this one.

I intend to contribute to the documentation on quantstrat, as I feel that is the weakest link.

jaymon0703 commented 3 years ago

You probably want to create your own custom indicator. Referring to how indicators work in the demos should suffice. You can use ROC together with anyone of the rollsum functions with n=3 and if the last 3 observations are negative, trend is down and indicator will be = -3. The signal will look for a sigThreshold where threshold=-3 and the relationship is "lte" for less than or equal to. Hope that helps.

jaymon0703 commented 3 years ago

Something like this:

require(quantstrat)
require(blotter)

symbols = c("SPY")
for(symbol in symbols){ # establish trade-able instruments
  stock(symbol, currency="USD",multiplier=1)
  getSymbols(symbol,src='yahoo')
}

stratTrend <- strategy("trend.st")

startDate='1997-12-31'
initEq=100000
port.st<-'trend.st'

# trend indicator
require(RcppRoll) # use roll_sumr from RcppRoll
n=3
trend <- function(price, n=n) {
  roc <- ROC(price)
  roc <- ifelse(roc<0, -1, 1)
  rollsum_roc <- roll_sumr(roc, n=3)
  return(rollsum_roc)
}

stratTrend <- add.indicator(strategy = stratTrend, name = "trend", arguments = list(price = quote(getPrice(mktdata)),n=n), label="trend")
applyIndicators(strategy = stratTrend, mktdata = SPY)

Gives the below indicator values: image

climbthemt commented 3 years ago

I was striving for something more user friendly using Lag. I thought I was going down the right path here, but there was a problem when combining signals.


# First Signal: sigComparison specifying when ticker closes Cl() 4 times lower than
#  previous then closes Cl() above previous high Hi()
Tick5<-(lag(Cl(mktdata), k=4))
Tick4<-(lag(Cl(mktdata), k=3))
Tick3<-(lag(Cl(mktdata), k=2))
Tick2<-(lag(Cl(mktdata), k=1))
Tick1<-(Cl(mktdata))
Tick1_low<-(Lo(mktdata))

add.signal(strategy.st, name = 'sigComparison',
             arguments = list(columns=c(Tick5, Tick4)),
             relationship = "lt",
             label = "drop5_4")

add.signal(strategy.st, name = 'sigComparison',
           arguments = list(columns=c(Tick4, Tick3)),
           relationship = "lt",
           label = "drop4_3")

add.signal(strategy.st, name = 'sigComparison',
           arguments = list(columns=c(Tick3, Tick2)),
           relationship = "lt",
           label = "drop3_2")

add.signal(strategy.st, name = 'sigComparison',
           arguments = list(columns=c(Tick1_low, Tick2)),
           relationship = "lt",
           label = "drop2_1")

add.signal(strategy.st, name = 'sigComparison',
           arguments = list(columns=c(Tick2, Tick1)),
           relationship = "lt",
           label = "bounce1")

add.signal(strategy.st, name = "sigFormula",
           arguments = list(formula = "((drop5_4 & drop4_3) & (drop3_2 & drop2_1)) & bounce1",
                            cross = TRUE),
           label = "longentry")
`
But it fails at 'sigFormula' because sigFormula cannot boolean compare more than 2 signals.
So how to combine multiple signals cleanly?  

Once I can get this to work nicely, I'll refactor and see if I can make a new function out of it.
jaymon0703 commented 3 years ago

IMHO i dont think 5 signals is user friendly, and using sigFormula should be avoided where possible as it will be slower than the native signal functions. A custom indicator makes more sense i think. Also, you dont need to use ROC (from TTR) but can get away with diff() from base R.

climbthemt commented 3 years ago

Ultimately, I'm looking to make a function that has a simple input that is easy for low level coders. ie. For the original example of: " If SPY closes down 3 days in a row then buy. Then sell if SPY closes above the previous high." The imagined input would be


buy_signal <- price_action(totalticks=3, action=list( " Cl(-3) > Cl(-2)", "Cl(-2)>Cl(-1)", "Cl(-1)>Cl() " )

sell_signal <- price_action((totalticks=2, action=list( " Hi(-1)<Cl() " )
`
jaymon0703 commented 3 years ago

Well, with a custom indicator you can set n to your preferred value, even using a range of values in parameter optimizations. A custom indicator is the best practice imo. Below is the same code, but with a signal added, from which you can add a rule to buy at the next bar, or whatever your data prescribes (BBO or bar data).

# Load Packages:

require(quantstrat)
require(blotter)

symbols = c("SPY")
for(symbol in symbols){ # establish trade-able instruments
  stock(symbol, currency="USD",multiplier=1)
  getSymbols(symbol,src='yahoo')
}

# Initialize Account, Portfolio, Strategy
stratTrend <- strategy("trend.st")

startDate='1997-12-31'
initEq=100000
port.st<-'trend.st' #use a string here for easier changing of parameters and re-trying

# trend indicator
require(RcppRoll) # use roll_sumr from RcppRoll
n=3
trend <- function(price, n=n) {
  roc <- ROC(price)
  roc <- ifelse(roc<0, -1, 1)
  rollsum_roc <- roll_sumr(roc, n=3)
  return(rollsum_roc)
}

# Indicator
stratTrend <- add.indicator(strategy = stratTrend, name = "trend", arguments = list(price = quote(getPrice(mktdata)),n=n), label="trend")
check_Indicators <- applyIndicators(strategy = stratTrend, mktdata = SPY)
check_Indicators

# Signal
stratTrend <- add.signal(strategy = stratTrend, name="sigThreshold",arguments = list(threshold=-3, column="trend",relationship="eq"),label="trend.eq.minus_three")
check_Signals <- applySignals(strategy = stratTrend, mktdata = applyIndicators(strategy=stratTrend, mktdata=SPY))
check_Signals

Signal output:

image

So first signal fires on 29/01/2007 which means you could open a trade the next day if your order was a market order, or whenever your limit price is reached in the case of a limit order.

climbthemt commented 3 years ago

Thank! That works. But as a demo example, I will need to add lots of comments to make it more universal and user friendly. I'm going to extend it out as a more broad example.

climbthemt commented 3 years ago

OK, so I'm hitting a few snags. It looks like there is a shortcoming in the documentation on applyIndicators I've taken a few days to hunt for clarity and found little.

In your code, you applyIndicators for the custom indicator. I was wondering what happens if we select several symbols like

symbols = c("SPY","IBM", "TSLA")

Then how do we apply to all of them? I couldn't find documentation on how how to format 'mktdata=(ALL)' parameter for applyindicators.

check_Indicators <- applyIndicators(strategy = stratTrend, mktdata = SPY)

I intend to update the documentation for applyIndicators, too. It is the minor knowledge holes that are such time eaters. I also thought it strange that the parameter was mktdata = SPY and not mktdata = 'SPY'

jaymon0703 commented 3 years ago

I for one am happy to see interest in the package, and willingness from the community to add to documentation is always encouraged, especially from the experience of someone newer to the package. We could always add to Tim Trice's document...we have reached out previously and asked if we could do so - https://timtrice.github.io/backtesting-strategies/

To run applyIndicators across a range of symbols and store in a list object for example, you can do something like:

ls_Indicators <- list()
for(symbol in symbols) {
  ls_Indicators[[symbol]] = applyIndicators(strategy = stratTrend, mktdata = get(symbol))
}

I tested it for 9 symbols of 3625 rows each and it still surprises me how remarkably quick the code is. I used symbols = c("XLF", "XLP", "XLE", "XLY", "XLV", "XLI", "XLB", "XLK", "XLU") which is from the RSI demo.

Of course, applyIndicators is predominantly for testing your code, and more specifically your indicators (and applySIgnals for testing your signals and applyRules for testing your rules). Therefore there are no portfolio considerations and position constraints etc that are taken into account yet. Those fire at the applyStrategy stage.

climbthemt commented 3 years ago

Hold on here, is 'applyIndicators' just a testing function that indicators are working. That 'applyIndicators' doesn't need to be run once you have your code debugged because 'applyStrategy' executes all 3: applyIndicators, applySignals, and applyRules? Do I have this correct?

jaymon0703 commented 3 years ago

Correct. Try debugonce(applyStrategy) before calling applyStrategy amd step through the guts of the logic.

Apply indicators is here - https://github.com/braverock/quantstrat/blob/6c536dbe97d0efb622c853d8d9445f9b2933b040/R/strategy.R#L162

Apply signals is here - https://github.com/braverock/quantstrat/blob/6c536dbe97d0efb622c853d8d9445f9b2933b040/R/strategy.R#L170

And depending on whether you have path.dependent set to TRUE or FALSE, apply rules is here - https://github.com/braverock/quantstrat/blob/6c536dbe97d0efb622c853d8d9445f9b2933b040/R/strategy.R#L193 or here - https://github.com/braverock/quantstrat/blob/6c536dbe97d0efb622c853d8d9445f9b2933b040/R/strategy.R#L219

climbthemt commented 3 years ago

Oi vey. I may not be a great R coder, but I can contribute to the docs. And there is much work to be done...