meeting goals, setting higher goals

In this release, we get a minor version bump because I feel we meet our goals of an easily traded (for small portfolios at least) quantitative strategy that bests the S&P 500 on a risk adjusted basis, and even (with a little leverage) beats that benchmark in total return with lower maximum drawdown and higher Sharpe ratio. Even without that leverage, it offers an almost competitive return over the past 15+ years at substantially lower portfolio risk. Those were our goals, so technically we’ve done it.

Maybe. Going forward, continuing that trend depends on continued outperformance by the tech sector. Seems plausible, but that’s a systemic risk. Instead, at the end this post we will set some new, unreasonably higher goals and continue our exploration of using realized volatility as a signal in creative (and profitable) new ways.

So let’s look at the changes for quant_rv 1.1.0.

### quant_rv v1.1.0 by babbage9010 and friends
# explore use of QQQ and of leverage
### released under MIT License

# Step 1: Load necessary libraries and data
library(quantmod)
library(PerformanceAnalytics)

date_start <- as.Date("2006-07-01")
date_end <- as.Date("2019-12-31")
symbol_bench1  <- "SPY"  # benchmark for comparison
symbol_signal1 <- "SPY"  # S&P 500 symbol (use SPY or ^GSPC)
symbol_trade1  <- "SPY"  # ETF to trade
symbol_trade2  <- "QQQ"  # ETF to trade

data_spy <- getSymbols(symbol_bench1, src = "yahoo", from = date_start, to = date_end, auto.assign = FALSE)
data_qqq <- getSymbols(symbol_trade2, src = "yahoo", from = date_start, to = date_end, auto.assign = FALSE)
prices_benchmark <- Ad(data_spy) #SPY ETF, Adjusted(Ad) for the benchmark
prices_signal <- Cl(data_spy) #SPY ETF, Close(Cl) for the signal (realized vol)
prices_trade1 <- Op(data_spy) #SPY data, Open(Op) for our trading
prices_trade2 <- Op(data_qqq) #QQQ data, Open(Op) for our trading

# Step 2: Calculate volatility as a risk indicator
lookback_period <- 20
roc_signal <-    ROC(prices_signal, n = 1, type = "discrete")
roc_benchmark <- ROC(prices_benchmark, n = 1, type = "discrete")
roc_trade1 <-    ROC(prices_trade1, n = 1, type = "discrete")
roc_trade2 <-    ROC(prices_trade2, n = 1, type = "discrete")
volatility <- runSD(roc_signal, n = lookback_period) * sqrt(252)

# Step 3: Develop the trading strategy
threshold <- 0.15
signal <- ifelse(volatility < threshold, 1, 0)
signal[is.na(signal)] <- 0

# Step 4: Backtest the strategies
leverage_strategy1 <- 1.0 #use this to simulate a leveraged ETF component
leverage_strategy2 <- 1.0
returns_strategy1 <- leverage_strategy1 * roc_trade1 * Lag(signal, 2)
returns_strategy1 <- na.omit(returns_strategy1)
returns_strategy2 <- leverage_strategy2 * roc_trade2 * Lag(signal, 2)
returns_strategy2 <- na.omit(returns_strategy2)

# Calculate benchmark returns
returns_benchmark <- roc_benchmark 
returns_benchmark <- Lag(returns_benchmark, 2)
returns_benchmark <- na.omit(returns_benchmark)

# Step 5: Evaluate performance and risk metrics
#switch the order to switch colors
comparison <- cbind(returns_strategy1, returns_benchmark, returns_strategy2)
colnames(comparison) <- c("Strategy1", "Benchmark", "Strategy2")
stats_rv <- rbind(table.AnnualizedReturns(comparison), maxDrawdown(comparison), AverageRecovery(comparison))

charts.PerformanceSummary(comparison, main = "Long/Flat Strategy vs S&P 500 Benchmark")

Ok, so what’s new?

  1. New default dates and rationale. date_start is now July 1 2006 and date_end is 2019-12-31. Yes, we can run it back to 2000 or farther (SPY opened in 1993), but I’ve chosen as our in-sample testing to run from 2006 to 2019, across the Great Financial Crisis and up the bull run, stopping before the Covid recession and our current bear/bull market. We can utilize earlier and later periods for out-of-sample testing in the future someday.
  2. Multiple symbols loaded. Instead of one “symbol” variable, we have several, easily identified as an attempt to allow usage of different trading instruments (ETFs in our case) for different purposes (benchmark distinct from signal-generating price series, etc). We’ve added “QQQ” into the symbols we’re exploring, and using it to trade a second strategy (cunningly named “strategy2”). QQQ is very popular with quant trading strategies because it has performed well since the mid-2000s, holds FANG and other tech stocks familiar to traders. But also, using a different instrument from SPY where we developed the strategy provides a small test of durability… does our basic signal work across markets?
  3. Leverage rough test. In “Step 4”, we’ve added a new variable for leverage. As you can see, it’s very easy to simulate the use of leveraged ETFs, even back when they weren’t available to trade. Eventually, we’ll use the actual leveraged instruments (like SSO) in our code, but for today, this simple leverage var will prove perfectly cromulent.
  4. Added Strategy2. Also in the code are new lines for calculating the backtest returns for strategy2, and putting strategy2 into the plot and stats routines.

So, let’s get to it. Here’s the plot for the QQQ in Strategy2 and you can spot the improvement pretty quickly.

Again, it’s a new X-axis time scale, without the Dot Com Bear market swoon and ending before Covid. But it’s easy to spot the improvement that QQQ brings to the volatility signal compared to using SPY. Here are the new stats:

                           Strategy1  Benchmark  Strategy2
Annualized Return          0.0407000  0.0933000  0.0796000
Annualized Std Dev         0.0864000  0.1901000  0.1127000
Annualized Sharpe (Rf=0%)  0.4711000  0.4911000  0.7061000
Worst Drawdown             0.1541298  0.5518946  0.1942727
Average Recovery          21.5058824 10.1491713 17.8080808

Strategy2 using QQQ flies ahead of our SPY benchmark for the first several years, gradually falling behind a bit, but with much lower drawdown and standard deviation, which reflects in the significantly higher Sharpe ratio. In fact, this begs the question: would a modest addition of leverage applied to Strategy2 create something that wins all around? We can test this easily with our new leverage_strategy2 variable. Let’s try leverage of 1.5x… that’s like using half our portfolio in QQQ and the other half in QLD (2x leveraged QQQ). We run that and the new table looks like this:

                           Strategy1  Benchmark  Strategy2
Annualized Return          0.0407000  0.0933000  0.1163000

Annualized Std Dev         0.0864000  0.1901000  0.1691000
Annualized Sharpe (Rf=0%)  0.4711000  0.4911000  0.6878000
Worst Drawdown             0.1541298  0.5518944  0.2816205
Average Recovery          21.5058824 10.1436464 18.4895833

Overall return: wins by 2 percentage points. Std dev: still lower than bench. Sharpe: still higher than bench. Worst drawdown: still only half that of bench. I declare a winner! Here’s the plot (strategy2 leads the benchmark virtually the entire way):

So… that was too easy. We already have a strategy that uses liquid index ETFs, readily available to retail investors (even in IRA accounts!), and handily beats the market with lower risk measurements. Granted there are some caveats, but not bad ones. We’ve not accounted for any trading costs, but commissions are $zero$ most everywhere these days, so that’s not much of a caveat. And, for a caveat in the other direction, we’re trading a dividend-bearing instrument and we’re invested roughly 60% of the trading days, so we should actually collect (and compound) 60% of the dividends that would accrue, a not insignificant boost.

Here were our stated goals at the end of June: Create a strategy that…

  1. develops a signal based on market close values and trades on the next-day Open (easy to trade)
  2. makes sense logically (not based on magic)
  3. can beat market benchmarks on a risk-adjusted basis, and hopefully on a CAGR basis

Check, check, check, and even the “hopefully” part, check. So, we need stretch goals here, we need to build a strategy that’s better, stronger, faster, all the things. I propose the following goals, and since I’m BDFL of this project, that’s what the goals will be, unless you my faithful companions, come up with some better goals that I happen to agree with.

Goals as of July 3. quant_rv will be a quantitative trading strategy that:

  1. trades popular, liquid ETFs (to allow it to scale meaningfully) with no extra leverage (no 2x or 3x ETFs)
  2. develops signals based on sensible, logical, statistically meaningful market observations (like realized volatility)
  3. trades at the next-day Open with signals based on the previous day’s market data (allowing plenty of time for followers to generate signals and place trades)
  4. unequivacably beats a benchmark of buy-and-hold SPY (including dividends compounded, ie., calculated using Adjusted Close) on all these metrics: Annual Return, Annualized Standard Deviation, Sharpe Ratio, Max Drawdown
  5. uses the same instruments as the benchmark in order to meet these goals (ie, SPY or derivatives/equivalents, not QQQ or some specific market sector ETF)
  6. accomplishes its goals without consideration of dividends collected by the strategy (quant_rv gets one hand tied behind its back)
  7. stretch goal: also performs reasonably well across several different market/ETF areas, to show that it really meets the #2 (sensible) goal above

There, I think that’s enough to get us going on a new phase of exploration for quant_rv. It’s all a little more explicit and verbose than the original, and I’ve italicized and underlined the clearly new parts. I think these are unreasonable goals, as there are few if any such strategies out in the public currently (are there? correct me with links?). Achievable goals, I believe. Let’s get going.



One response to “meeting goals, setting higher goals”

  1. […] we advance our efforts toward our goals in this post? Well, we certainly didn’t beat any benchmarks today, but that wasn’t […]

    Like

Leave a comment

Blog at WordPress.com.

Design a site like this with WordPress.com
Get started