book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

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.Valuecurrent.value property output of one indicator is the input of the following indicator. To chain indicators together with values other than the Current.Valuecurrent.value property, create a custom indicator.

Addition

The Plusplus extension sums the Current.Valuecurrent.value property of two indicators or sums the Current.Valuecurrent.value property of an indicator and a fixed value.

// Sum the output of two indicators
var min = MIN("SPY", 21);
var std = STD("SPY", 21);
var minPlusStd = min.Plus(std);

// Sum the output of an indicator and a fixed value
var rsiPlusValue = min.Plus(10);
# 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 Plusplus extension, you can name the composite indicator.

var namedIndicator = min.Plus(std, "Buy Zone");
named_indicator = IndicatorExtensions.plus(min_, std, "Buy Zone")

Subtraction

The Minusminus extension subtracts the Current.Valuecurrent.value property of two indicators or subtracts a fixed value from the Current.Valuecurrent.value property of an indicator.

// Subtract the output of two indicators
var smaShort = SMA("SPY", 14);
var smaLong = SMA("SPY", 21);
var smaDifference = smaShort.Minus(smaLong);

// Subtract a fixed value from the output of an indicator
var smaMinusValue = smaShort.Minus(10);
# 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 Minusminus extension, you can name the composite indicator.

var namedIndicator = smaShort.Minus(smaLong, "SMA Difference");
named_indicator = IndicatorExtensions.minus(sma_short, sma_long, "SMA Difference")

Multiplication

The Timestimes extension multiplies the Current.Valuecurrent.value property of two indicators or multiplies a fixed value and the Current.Valuecurrent.value property of an indicator.

// Multiply the output of two indicators
var emaShort = EMA("SPY", 14);
var emaLong = EMA("SPY", 21);
var emaProduct = emaShort.Times(emaLong);

// Multiply the output of an indicator and a fixed value
var emaTimesValue = emaShort.Times(10);
# 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 Timestimes property extension, you can name the composite indicator.

var namedIndicator = emaShort.Times(emaLong, "EMA Product");
named_indicator = IndicatorExtensions.times(ema_short, ema_long, "EMA Product")

Division

The Overover extension divides the Current.Valuecurrent.value property of an indicator by the Current.Valuecurrent.value property of another indicator or a fixed value.

// Divide the output of two indicators
var rsiShort = RSI("SPY", 14);
var rsiLong = RSI("SPY", 21);
var rsiDivision = rsiShort.Over(rsiLong);

// Divide the output of an indicator by a fixed value
var rsiAverage = rsiShort.Plus(rsiLong).Over(2);
# 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 Overover extension, you can name the composite indicator.

var namedIndicator = rsiShort.Over(rsiLong, "RSI Division");
named_indicator = IndicatorExtensions.over(rsi_short, rsi_long, "RSI Division")

Weighted Average

The WeightedByweighted_by extension calculates the average Current.Valuecurrent.value property of an indicator over a lookback period, weighted by another indicator over the same lookback period. The value of the calculation is

$$ \frac{\textbf{x} \cdot \textbf{y}}{ \sum\limits_{i=1}^{n} y_{i} } $$

where $\textbf{x}$ is a vector that contains the historical values of the first indicator, $\textbf{y}$ is a vector that contains the historical values of the second indicator, and $n$ is the lookback period.

var smaShort = SMA("SPY", 14); 
var smaLong = SMA("SPY", 21); 
var weightedSMA = smaShort.WeightedBy(smaLong, 3);
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 Ofof extension feeds an indicator's Current.Valuecurrent.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.

var rsi = RSI("SPY", 14);
var rsiSMA = (new SimpleMovingAverage(10)).Of(rsi); // 10-period SMA of the 14-period RSI
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 Updateupdate method of the entire indicator chain, it won't update the chain properly.

Simple Moving Average

The SMAsma extension calculates the simple moving average of an indicator's Current.Valuecurrent.value property.

var rsi = RSI("SPY", 14); // Create a RSI indicator
var rsiSMA = rsi.SMA(3); // Create an indicator to calculate the 3-period SMA of the RSI indicator
rsi = self.rsi("SPY", 14) # Create a RSI indicator
rsi_sma = IndicatorExtensions.SMA(rsi, 3) # Create an indicator to calculate the 3-period SMA of the RSI indicator

Exponential Moving Average

The EMAema extension calculates the exponential moving average of an indicator's Current.Valuecurrent.value property.

var rsi = RSI("SPY", 14); // Create a RSI indicator
var rsiEMA = _rsi.EMA(3); // Create an indicator to calculate the 3-period EMA of the RSI indicator
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 EMAema extension can also accept a smoothing parameter that sets the percentage of data from the previous value that's carried into the next value.

var rsiEMA = rsi.EMA(3, 0.1m); // 10% smoothing factor
rsi_ema = IndicatorExtensions.EMA(rsi, 3, 0.1) # 10% smoothing factor

Maximum

The MAXmax extension calculates an indicator's maximum Current.Valuecurrent.value property over a lookback window.

var ema = EMA("SPY", 14); // Create an EMA indicator
var emaMax = ema.MAX(10); // Create an indicator to calculate the maximum EMA over the last 10 periods
ema = self.ema("SPY", 14) # Create an EMA indicator
ema_max = IndicatorExtensions.MAX(ema, 10) # Create an indicator to calculate the maximum EMA over the last 10 periods

Minimum

The MINmin extension calculates an indicator's minimum Current.Valuecurrent.value property over a lookback window.

var ema = EMA("SPY", 14); // Create an EMA indicator
var emaMin = ema.MIN(10); // Create an indicator to calculate the minimum EMA over the last 10 periods
ema = self.ema("SPY", 14) # Create an EMA indicator
ema_min = IndicatorExtensions.MIN(ema, 10) # Create an indicator to calculate the minimum EMA over the last 10 periods

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.

public class CombiningIndicatorsAlgorithm : QCAlgorithm
{
    private Symbol _spy, _option;
    private SimpleMovingAverage _sma;
    private StandardDeviation _sd;

    public override void Initialize()
    {
        SetStartDate(2020, 1, 1);
        SetEndDate(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.
        _spy = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;

        // Request option data to trade.
        var option = AddOption(_spy);
        _option = option.Symbol;
        // Filter for 7-day expiring options with $5 apart from the current price to trade volatility using strangle.
        option.SetFilter((universe) => universe.IncludeWeeklys().Strangle(7, 5, -5));

        // Create a return indicator to get the daily return of SPY.
        var ret = ROC(_spy, 1, Resolution.Daily);
        // Create an SD indicator to measure the 252-day SD of return to measure SPY's volatility.
        _sd = IndicatorExtensions.Of(new StandardDeviation(252), ret);
        // Create a 20-day SMA indicator of the SD indicator to compare the average volatility.
        _sma = IndicatorExtensions.Of(new SimpleMovingAverage(20), _sd);

        // Warm up for immediate usage of indicators.
        SetWarmUp(400, Resolution.Daily);
    }

    public override void OnData(Slice slice)
    {
        if (!Portfolio.Invested && slice.OptionChains.TryGetValue(_option, out var chain))
        {
            // Create a strangle strategy to trade the volatility forecast.
            var otmCallStrike = chain.Max(x => x.Strike);
            var otmPutStrike = chain.Min(x => x.Strike);
            var expiry = chain.Min(x => x.Expiry);
            var strangle = OptionStrategies.Strangle(_option, otmCallStrike, otmPutStrike, 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 (_sd > _sma)
            {
                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.
            if (_sd < _sma)
            {
                Sell(strangle, 2);
            }
        }
        else if (Portfolio[_spy].Invested)
        {
            // Liquidate any assigned underlying positions.
            Liquidate(_spy);
        }
    }
}
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 to confirm the trend better.Of IndicatorExtensions.of method to create a Delay indicator on SMA indicator.

public class DisplacedMovingAverageRibbon : QCAlgorithm
{
    private Symbol _spy;
    private IndicatorBase<IndicatorDataPoint>[] _ribbon;

    public override void Initialize()
    {
        SetStartDate(2009, 1, 1);
        SetEndDate(2015, 1, 1);

        // Request daily SPY data for feeding indicator and trading.
        _spy = AddEquity("SPY", Resolution.Daily).Symbol;

        // Create 6 15-day SMA indicators, with a 5-day delay between each indicator.
        const int count = 6;
        const int offset = 5;
        const int period = 15;

        // Define our sma as the base of the ribbon.
        var sma = new SimpleMovingAverage(period);

        _ribbon = Enumerable.Range(0, count).Select(x =>
        {
            // Define our offset to the zero SMA; these various offsets will create our 'displaced' ribbon.
            var delay = new Delay(offset*(x+1));
            // Using Delay indicator to create displaced SMA indicators.
            var delayedSma = delay.Of(sma);
            // Register our new 'delayed_sma' for automatic updates on a daily resolution.
            RegisterIndicator(_spy, delayedSma, Resolution.Daily, data => data.Value);
            return delayedSma;
        }).ToArray();
    }

    public override void OnData(Slice slice)
    {
        // Trade only on updated data with ready-to-use indicators.
        if (!_ribbon.All(x => x.IsReady)) return;
        if (!slice.Bars.TryGetValue(_spy, out var data))
        {
            return;
        }
        // Plot indicators each time they update.
        Plot("Ribbon", "Price", data.Price);
        Plot("Ribbon", _ribbon);

        var values = _ribbon.Select(x => x.Current.Value).ToArray();
        var holding = Portfolio[_spy];
        // Buy SPY if the trend is upward.
        if (holding.Quantity <= 0 && IsAscending(values))
        {
            SetHoldings(_spy, 1.0);
        }
        // Liquidate if the trend is downwards.
        else if (holding.Quantity > 0 && IsDescending(values))
        {
            Liquidate(_spy);
        }
    }

    // Returns true if the SMA values are in ascending order, indicating an upward trend
    private bool IsAscending(IEnumerable<decimal> values)
    {
        decimal? last = null;
        foreach (var val in values)
        {
            if (last == null)
            {
                last = val;
                continue;
            }

            if (last.Value < val)
            {
                return false;
            }
            last = val;
        }
        return true;
    }

    // Returns true if the SMA values are in descending order, indicating a downward trend
    private bool IsDescending(IEnumerable<decimal> values)
    {
        decimal? last = null;
        foreach (var val in values)
        {
            if (last == null)
            {
                last = val;
                continue;
            }

            if (last.Value > val)
            {
                return false;
            }
            last = val;
        }
        return true;
    }
}
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:

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: