Indicators
Combining Indicators
Introduction
Indicator extensions let you chain indications together like Lego blocks to create unique combinations. When you chain indicators together, the current.value
property output of one indicator is the input of the following indicator. To chain indicators together with values other than the current.value
property, create a custom indicator.
Addition
The plus
extension sums the current.value
property of two indicators or sums the current.value
property of an indicator and a fixed value.
# Sum the output of two indicators min_ = self.min("SPY", 21) std = self.std("SPY", 21) min_plus_std = IndicatorExtensions.plus(min_, std) # Sum the output of an indicator and a fixed value min_plus_value = IndicatorExtensions.plus(min_, 10)
If you pass an indicator to the plus
extension, you can name the composite indicator.
named_indicator = IndicatorExtensions.plus(min_, std, "Buy Zone")
Subtraction
The minus
extension subtracts the current.value
property of two indicators or subtracts a fixed value from the current.value
property of an indicator.
# Subtract the output of two indicators sma_short = self.sma("SPY", 14) sma_long = self.sma("SPY", 21) sma_difference = IndicatorExtensions.minus(sma_short, sma_long) # Subtract a fixed value from the output of an indicator sma_minus_value = IndicatorExtensions.minus(sma_short, 10)
If you pass an indicator to the minus
extension, you can name the composite indicator.
named_indicator = IndicatorExtensions.minus(sma_short, sma_long, "SMA Difference")
Multiplication
The times
extension multiplies the current.value
property of two indicators or multiplies a fixed value and the current.value
property of an indicator.
# Multiply the output of two indicators ema_short = self.ema("SPY", 14) ema_long = self.ema("SPY", 21) ema_product = IndicatorExtensions.times(ema_short, ema_long) # Multiply the output of an indicator and a fixed value ema_times_value = IndicatorExtensions.times(ema_short, 1.5)
If you pass an indicator to the
extension, you can name the composite indicator.times
property
named_indicator = IndicatorExtensions.times(ema_short, ema_long, "EMA Product")
Division
The over
extension divides the current.value
property of an indicator by the current.value
property of another indicator or a fixed value.
# Divide the output of two indicators rsi_short = self.rsi("SPY", 14) rsi_long = self.rsi("SPY", 21) rsi_division = rsi_short.over(rsi_long) # Divide the output of an indicator by a fixed value rsi_half = IndicatorExtensions.over(rsi_short, 2)
If you pass an indicator to the over
extension, you can name the composite indicator.
named_indicator = IndicatorExtensions.over(rsi_short, rsi_long, "RSI Division")
Weighted Average
The weighted_by
extension calculates the average current.value
property of an indicator over a lookback period, weighted by another indicator over the same lookback period. The value of the calculation is
where x is a vector that contains the historical values of the first indicator, y is a vector that contains the historical values of the second indicator, and n is the lookback period.
sma_short = self.sma("SPY", 14) sma_long = self.sma("SPY", 21) weighted_sma = IndicatorExtensions.weighted_by(sma_short, sma_long, 3)
Custom Chains
The of
extension feeds an indicator's current.value
property into the input of another indicator. The first argument of the IndicatorExtensions.Of
method must be a manual indicator with no automatic updates. If you pass an indicator that has automatic updates as the argument, that first indicator is updated twice. The first update is from the security data and the second update is from the IndicatorExtensions
class.
rsi = self.rsi("SPY", 14) rsi_sma = IndicatorExtensions.of(SimpleMovingAverage(10), rsi) # 10-period SMA of the 14-period RSI
If you pass a manual indicator as the second argument, to update the indicator chain, update the second indicator. If you call the update
method of the entire indicator chain, it won't update the chain properly.
Exponential Moving Average
The ema
extension calculates the exponential moving average of an indicator's current.value
property.
rsi = self.rsi("SPY", 14) # Create a RSI indicator rsi_ema = IndicatorExtensions.EMA(rsi, 3) # Create an indicator to calculate the 3-period EMA of the RSI indicator
The ema
extension can also accept a smoothing parameter that sets the percentage of data from the previous value that's carried into the next value.
rsi_ema = IndicatorExtensions.EMA(rsi, 3, 0.1) # 10% smoothing factor
Examples
The following examples demonstrate some common practices for combining indicators.
Example 1: Volatility
The following algorithm trades a volatility strategy. By comparing SMA and the current value of the standard deviation of the return, we can estimate the current volatility regime is above or below average to trade the price volatility through strangle.
class CombiningIndicatorsAlgorithm(QCAlgorithm): def initialize(self) -> None: self.set_start_date(2020, 1, 1) self.set_end_date(2020, 6, 1) # Request daily SPY data to feed the indicators and generate trade signals. # Use Raw data normalization mode to compare the strike price fairly. self.spy = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol # Request option data to trade. option = self.add_option(self.spy) self._option = option.symbol # Filter for 7-day expiring options with $5 apart from the current price to trade volatility using strangle. option.set_filter(lambda universe: universe.include_weeklys().strangle(7, 5, -5)) # Create a return indicator to get the daily return of SPY. ret = self.roc(self.spy, 1, Resolution.DAILY) # Create an SD indicator to measure the 252-day SD of return to measure SPY's volatility. self._sd = IndicatorExtensions.of(StandardDeviation(252), ret) # Create a 20-day SMA indicator of the SD indicator to compare the average volatility. self._sma = IndicatorExtensions.of(SimpleMovingAverage(20), self._sd) # Warm up for immediate usage of indicators. self.set_warm_up(400, Resolution.DAILY) def on_data(self, slice: Slice) -> None: chain = slice.option_chains.get(self._option) if not self.portfolio.invested and chain: # Create a strangle strategy to trade the volatility forecast. sorted_strike = sorted([x.strike for x in chain]) otm_call_strike = sorted_strike[-1] otm_put_strike = sorted_strike[0] expiry = list(chain)[0].expiry strangle = OptionStrategies.strangle(self._option, otm_call_strike, otm_put_strike, expiry) # If the current STD is above its SMA, we estimate the volatility will remain high due to volatility clustering. # Thus, we long the strangle to earn from the price displacement from the current level. if self._sd.current.value > self._sma.current.value: self.buy(strangle, 2) # If the current STD is below its SMA, we estimate the volatility will remain lower due to volatility clustering. # Thus, we short the strangle to earn from the price staying at the current level. elif self._sd.current.value < self._sma.current.value: self.sell(strangle, 2) elif self.portfolio[self.spy].invested: # Liquidate any assigned underlying positions. self.liquidate(self.spy)
Example 2: Displaced SMA Ribbon
The following algorithm trades trends indicated by SMA crossings. We use the
IndicatorExtensions.of
method to create a Delay indicator on SMA indicator.
class DisplacedMovingAverageRibbon(QCAlgorithm): def initialize(self) -> None: self.set_start_date(2009, 1, 1) self.set_end_date(2015, 1, 1) # Request daily SPY data for feeding indicator and trading. self.spy = self.add_equity("SPY", Resolution.DAILY).symbol # Create 6 15-day SMA indicators, with a 5-day delay between each indicator. count = 6 offset = 5 period = 15 self.ribbon = [] # Define our sma as the base of the ribbon. self.sma = SimpleMovingAverage(period) for x in range(count): # Define our offset to the zero SMA. These various offsets will create our 'displaced' ribbon. delay = Delay(offset*(x+1)) # Using Delay indicator to create displaced SMA indicators. delayed_sma = IndicatorExtensions.of(delay, self.sma) # Register our new 'delayed_sma' for automatic updates on a daily resolution. self.register_indicator(self.spy, delayed_sma, Resolution.DAILY) self.ribbon.append(delayed_sma) # Plot indicators each time they update using the PlotIndicator function. for i in self.ribbon: self.plot_indicator("Ribbon", i) def on_data(self, data: Slice) -> None: # Trade only on updated data with ready-to-use indicators. if data[self.spy] is None: return if not all(x.is_ready for x in self.ribbon): return self.plot("Ribbon", "Price", data[self.spy].price) values = [x.current.value for x in self.ribbon] holding = self.portfolio[self.spy] # Buy SPY if the trend is upward. if (holding.quantity <= 0 and self.is_ascending(values)): self.set_holdings(self.spy, 1.0) # Liquidate if the trend is downwards. elif (holding.quantity > 0 and self.is_descending(values)): self.liquidate(self.spy) # Returns true if the SMA values are in ascending order, indicating an upward trend def is_ascending(self, values: List[float]) -> None: last = None for val in values: if last is None: last = val continue if last < val: return False last = val return True # Returns true if the SMA values are in descending order, indicating a downward trend def is_descending(self, values: List[float]) -> None: last = None for val in values: if last is None: last = val continue if last > val: return False last = val return True
Other Examples
For more examples, see the following algorithms: