Skip to content
All library documents

MACD Alpha Signals with a Normalized Bounce Threshold

Article Strategy library · Author: QuantConnect

Summary

This alpha model turns MACD signal-line values into directional price insights. For each security with a nonzero price, it divides the MACD signal value by the current price and compares the result with a one-percent threshold. Values beyond the positive or negative boundary produce up or down insights; values inside the boundary are treated as flat. Repeated signals in the same direction are skipped, while a flat reading cancels outstanding insights.

The implementation maintains MACD indicators per security, warms them up at the chosen resolution, and removes consolidators and related insights when securities leave the universe. The default MACD periods are 12, 26, and 9, with daily resolution and an insight horizon based on the fast period. This is a framework example rather than an evaluated trading strategy: it provides no backtest results, and does not establish that the threshold or signal mapping is profitable.

Key ideas

  • The model normalizes the MACD signal-line value by the security price before applying its threshold.
  • Signals beyond the positive or negative threshold generate up or down insights, while values inside it map to flat.
  • It suppresses a repeated direction and cancels existing insights when the state returns to flat.
  • Per-security indicators are initialized and warmed up, then cleaned up when securities are removed.
  • The example specifies default periods but presents no evidence of trading performance.

Tags

Full text
# MacdAlphaModel


# MacdAlphaModel









Defines a custom alpha model that uses MACD crossovers. The MACD signal line
    is used to generate up/down insights if it's stronger than the bounce threshold.
    If the MACD signal is within the bounce threshold then a flat price insight is returned.

## Source (Apache-2.0)

```python
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *

class MacdAlphaModel(AlphaModel):
    '''Defines a custom alpha model that uses MACD crossovers. The MACD signal line
    is used to generate up/down insights if it's stronger than the bounce threshold.
    If the MACD signal is within the bounce threshold then a flat price insight is returned.'''

    def __init__(self,
                 fastPeriod = 12,
                 slowPeriod = 26,
                 signalPeriod = 9,
                 movingAverageType = MovingAverageType.Exponential,
                 resolution = Resolution.Daily):
        ''' Initializes a new instance of the MacdAlphaModel class
        Args:
            fastPeriod: The MACD fast period
            slowPeriod: The MACD slow period</param>
            signalPeriod: The smoothing period for the MACD signal
            movingAverageType: The type of moving average to use in the MACD'''
        self.fastPeriod = fastPeriod
        self.slowPeriod = slowPeriod
        self.signalPeriod = signalPeriod
        self.movingAverageType = movingAverageType
        self.resolution = resolution
        self.insightPeriod = Time.Multiply(Extensions.ToTimeSpan(resolution), fastPeriod)
        self.bounceThresholdPercent = 0.01
        self.insightCollection = InsightCollection()
        self.symbolData = {}

        self.Name = '{}({},{},{},{},{})'.format(self.__class__.__name__, fastPeriod, slowPeriod, signalPeriod, movingAverageType, resolution)


    def Update(self, algorithm, data):
        ''' Determines an insight for each security based on it's current MACD signal
        Args:
            algorithm: The algorithm instance
            data: The new data available
        Returns:
            The new insights generated'''
        insights = []

        for key, sd in self.symbolData.items():
            if sd.Security.Price == 0:
                continue

            direction = InsightDirection.Flat
            normalized_signal = sd.MACD.Signal.Current.Value / sd.Security.Price

            if normalized_signal > self.bounceThresholdPercent:
                direction = InsightDirection.Up
            elif normalized_signal < -self.bounceThresholdPercent:
                direction = InsightDirection.Down

            # ignore signal for same direction as previous signal
            if direction == sd.PreviousDirection:
                continue

            sd.PreviousDirection = direction

            if direction == InsightDirection.Flat:
                self.CancelInsights(algorithm, sd.Security.Symbol)
                continue

            insight = Insight.Price(sd.Security.Symbol, self.insightPeriod, direction)
            insights.append(insight)
            self.insightCollection.Add(insight)

        return insights


    def OnSecuritiesChanged(self, algorithm, changes):
        '''Event fired each time the we add/remove securities from the data feed.
        This initializes the MACD for each added security and cleans up the indicator for each removed security.
        Args:
            algorithm: The algorithm instance that experienced the change in securities
            changes: The security additions and removals from the algorithm'''
        for added in changes.AddedSecurities:
            self.symbolData[added.Symbol] = SymbolData(algorithm, added, self.fastPeriod, self.slowPeriod, self.signalPeriod, self.movingAverageType, self.resolution)

        for removed in changes.RemovedSecurities:
            symbol = removed.Symbol

            data = self.symbolData.pop(symbol, None)
            if data is not None:
                # clean up our consolidator
                algorithm.SubscriptionManager.RemoveConsolidator(symbol, data.Consolidator)

            # remove from insight collection manager
            self.CancelInsights(algorithm, symbol)

    def CancelInsights(self, algorithm, symbol):
        if not self.insightCollection.ContainsKey(symbol):
            return
        insights = self.insightCollection[symbol]
        algorithm.Insights.Cancel(insights)
        self.insightCollection.Clear([ symbol ]);


class SymbolData:
    def __init__(self, algorithm, security, fastPeriod, slowPeriod, signalPeriod, movingAverageType, resolution):
        self.Security = security
        self.MACD = MovingAverageConvergenceDivergence(fastPeriod, slowPeriod, signalPeriod, movingAverageType)

        self.Consolidator = algorithm.ResolveConsolidator(security.Symbol, resolution)
        algorithm.RegisterIndicator(security.Symbol, self.MACD, self.Consolidator)
        algorithm.WarmUpIndicator(security.Symbol, self.MACD, resolution)

        self.PreviousDirection = None

```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.