Trade Smarter with Automation

Complex Indicators

Overview

This tutorial will show you how to create a strategy that trades when it finds a specific pattern in the market. The strategy will use an intraday chart with two technical indicators - Bollinger Bands and the Moving Average - to scan for trends.

My father-in-law Ray, a veteran stockbroker, has observed that whenever a price dips below a lower Bollinger Band, usually the moving average will drop a little further and then turn and increase. Our goal is to buy after N points into that upswing. The chart below shows the events we want to monitor in the correct order for entry. Please note that this tutorial is not an endorsement of any particular strategy, but he has been using this strategy for many years successfully.

Trader Scans for this Pattern

Strategy

Set Configs

We're building upon the api outlined in Strategy Basics. There you'll see that a good place to start is to set the configs. First we add the name, description, and positionType. We'll hardcode target because we only plan on running this with the SPY.

We specify how to enter and exit a position with the enterAction and exitAction configs. Finally, we'll avoid the price volatility of the last 15 minutes of trading with the closeTime config, which stops the trader M minutes before the end of each day.


name: 'Enter Low BBand Trend',
description: 'If price crosses lower BBand, buy after N points into an upswing. Exit is inverse of entry',
positionType: 'Equity',
target: 'SPY',

enterAction: Trade.BUY,
exitAction: Trade.SELL,

closeTime: -15,

Add Parameters

Next we'll add the parameters to our strategy. Parameters are great because they make it easy to launch different traders with slightly different settings. They give us form fields on the 'New Trader' screen to tweak the values without reopening the code.

To determine what values we'll want to parameterize, we have to think through all the chart indicators and other elements the strategy will use. Here's our list (you may want to scroll ahead to see what these elements are):
- points away from malow / mahigh
- amount of stop loss
- most of the chart indicator inputs:
     - bbands time period
     - bbands standard deviation up
     - bbands standard deviation down
     - moving average time period

To access a param value as a String from worker configs, use $ + the id of the param. As an example from the code below, '$devup' will reference the value from 'BBands SD Up' in our chart indicator because that param has an id of 'devup'.

To access a param value from inside a template method, use the data Object + whatever id you gave the param. For example, data.points returns the 'Points from MA' value because we assigned that param an id of points.


params: [{
        id: 'points',
        type: 'number',
        label: 'Points from MA',
        min: 0,
        max: 5,
        required: true,
        allowDecimals: true,
        defaultValue: .04,
        step: .01,
        description: 'After the moving average bottoms, this is how many points to wait for it to rise before entering position. Also used inversely for exit.'
    }, {
        id: 'stoploss',
        type: 'percent',
        label: 'Stop Loss',
        description: 'The percentage a position is allowed to lose before being exited.',
        required: true,
        allowDecimals: true,
        decimalPrecision: 2,
        defaultValue: 1
    }, {
        id: 'devup',
        type: 'number',
        label: 'BBands SD Up',
        min: 0,
        max: 5,
        required: true,
        allowDecimals: true,
        defaultValue: 2,
        step: .01,
        group: 'Chart Settings',
        description: 'Bollinger Bands Standard Deviation Up'
    }, {
        id: 'devdn',
        type: 'number',
        label: 'BBands SD Down',
        min: 0,
        max: 5,
        required: true,
        allowDecimals: true,
        defaultValue: 2,
        step: .01,
        description: 'Bollinger Bands Standard Deviation Down'
    }, {
        id: 'bbperiod',
        type: 'number',
        label: 'BBands Time Period',
        min: 0,
        max: 100,
        required: true,
        allowDecimals: false,
        defaultValue: 5,
        step: 1,
        description: 'Bollinger Bands Time Period'
    }, {
        id: 'maperiod',
        type: 'number',
        label: 'Moving Average Time Period',
        min: 0,
        max: 100,
        required: true,
        allowDecimals: false,
        defaultValue: 30,
        step: 1,
        description: 'Moving Average Time Period'
    }],

Chart Indicators

Our strategy scans the aforementioned indicators on the SPY's intraday chart. This requires first adding an ichart to the workers array, with the interval set to receive a new bar every minute. Notice that we've set the chart to use a minimum of 20 bars, which means that its data will first be ready 20 minutes after the market opens.

'$target' is a built-in handle for a strategy's target security


workers: [{
    id: 'todayChart'
    type: 'ichart',
    security: '$target',
    interval: Chart.Interval.MINUTE,
    minBars: 20
}],

Bollinger Bands

Now we'll add the actual indicators. It's as easy as nesting config objects inside the indicators array. For the Bollinger Bands, we're referencing the parameter values for the time period as well as for standard deviation up and down.


indicators: [{
    type: 'bbands',
    timePeriod: '$bbperiod',
    nbDevUp: '$devup',
    nbDevDn: '$devdn',
    maType: Chart.SMA
}]

Moving Average

We'll use the simple moving average across a time period of $maperiod (from params) for our chart.


{
    type: 'sma',
    timePeriod: '$maperiod'
}

This is what our entire chart worker looks like now:


workers: [{
    id: 'todayChart',
    type: 'ichart',
    security: '$target',
    interval: Chart.Interval.MINUTE,
    minBars: 20,
    indicators: [{
        type: 'bbands',
        timePeriod: '$bbperiod',
        nbDevUp: '$devup',
        nbDevDn: '$devdn',
        maType: Chart.SMA
    }, {
        type: 'sma',
        timePeriod: '$maperiod'
    }]
}

Enter Trade

The logic for when to enter a trade belongs in the onSeek method. This code will run every tick, or whenever there's new data available, until it successfully enters a position (then onManage takes over). The top of our onSeek block will be full of local vars referencing the data from chart bars, indicators, and params we have set the stage with. Then we have some if / else conditions to get to our trade.

Trader Data

Every Trader has a data object that holds parameter values, charts, option chains, etc that the Trader and strategy will use. We can also use it to store custom values for our strategy at run-time.

Notice in the code below how we access the chart by referencing todayChart in the Trader's data. The Chart object is available in the Trader's data because the chart worker above has an id of 'todayChart'. Parameter values and worker data (charts, option chains, etc) are available in data by the id property set on the parameter or worker config.

We reference the last bar of the chart because we need its low value. And we access the Bollinger Bands and Moving Average chart indicators using bar.get('myindicator') syntax.


onSeek: function(target, data){
    var chart = data.todayChart,
        points = data.points,
        lbar = chart.lastBar,
        lband = lbar.indicator('bbands', 'lowerBand'),
        ma = lbar.indicator('sma');

    ...

Storing Data

The pattern we are scanning for consists of 3 distinct events:
1. the lower BBand being crossed by the last bar's low price
2. the moving average hitting bottom
3. the moving average moving up N points

The first event will set a flag in data, the second will change the flag as well as store the moving average value for comparison, and the third event will trigger a trade. To store these values, we'll add them to the data object as status and malow (for the moving average low).

You see what values are in a Trader's data object at any time by selecting the Trader in the Debug tab of Strategy Studio.

With these flags in place, the logic for entering a trade is a matter of checking and updating the status, comparing values as the market changes:


onSeek: function(target, data){
    var chart = data.todayChart,
        points = data.points,
        lbar = chart.lastBar,
        lband = lbar.indicator('bbands', 'lowerBand'),
        ma = lbar.indicator('sma'),
        status = data.status,
        malow = data.malow;

    // crossed lowerband, reset
    if(lbar.low < lband){
        data.status = 'wait-for-ma-bottom';
        data.malow = ma;

        // wait for ma to bottom
    } else if(status === 'wait-for-ma-bottom'){
        if(ma < malow){
            data.malow = ma;
        } else {
            data.status = 'wait-for-ma-rise';
        }

        // wait for rise
    } else if(status === 'wait-for-ma-rise'){
        var signal = malow + points;
        if(ma > signal){
            var memo = 'Buy: Price crossed Lower Bband, then MA:{0}, MA Low:{1}'.format(ma, signal - points);
            this.enter(target, memo);
            delete data.status;
        }
    }
}

Notice that we delete data.status after we enter a trade to reset the state. We'll also start with a clean state every morning by hooking into the onMarketOpen template method.


onMarketOpen: function(){
    delete this.data.status;  // start fresh daily
},

Exit Position

Ray says that "Getting out is even more important than getting in". The code for exiting a position belongs in the onManage template method. For our strategy, we're simply going to apply the inverse logic we used to enter the position: We want to sell when the SPY has reached (we presume) a high point. And just as the low price exceeding a lower Bollinger Band is our oversold signal, the high price exceeding the upper Bollinger Band is our overbought signal.

Compare the onManage method below to the onSeek method above and notice what we changed - the upper bband instead of lower, the last bar high instead of low, statuses of waiting for moving average peak / drop, and the direction of the comparisons.


onManage: function(target, position, data){

    var chart = data.todayChart,
        points = data.points,
        lbar = chart.lastBar,
        uband = lbar.indicator('bbands', 'upperBand'),
        ma = lbar.indicator('sma'),
        status = data.status,
        mahigh = data.mahigh,
        gain = position.gain * 100;

    // stoploss
    if(gain <= -data.stoploss){
        return this.exit(position, 'Stop loss reached - {0}%'.format(gain));
    }

    // crossed upperband, reset
    if(lbar.high > uband){
        data.status = 'wait-for-ma-peak';
        data.mahigh = ma;

        // wait for ma to peak
    } else if(status === 'wait-for-ma-peak'){
        var high = mahigh;
        if(ma > high){
            data.mahigh = ma;
        } else {
            data.status = 'wait-for-ma-drop';
        }

        // wait for drop
    } else if(status === 'wait-for-ma-drop'){
        var signal = mahigh - points;
        if(ma < signal){
            var memo = 'Sell: Price crossed Upper Bband, then MA:{0}, MA High:{1}'.format(ma, signal + points);
            this.exit(position, memo);
            delete data.status;
        }
    }
}

Stop Loss

We'll also add a stop loss so that we exit a position automatically if it loses X percent of value. First we need a parameter to set X:


{
    id: 'stoploss',
    type: 'percent',
    label: 'Stop Loss',
    description: 'The percentage a position is allowed to lose before being exited.',
    required: true,
    allowDecimals: true,
    decimalPrecision: 2,
    defaultValue: 1
}

And then add the condition to our onManage method. gain is a property of position:


var gain = position.gain * 100;

// stoploss
if(gain <= -data.stoploss){
    return this.exit(position, 'Stop loss reached - {0}%'.format(gain));
}

Full Sample Code

In this tutorial we assembled configs and a chart worker with both Bollinger Bands and a Moving Average indicator to make a fully-runnable strategy. We used the onSeek method to enter a position and onManage to exit. We also put in a stop loss, as well as avoided the end-of-day volatility. And we glued it all together with params.

The Alta5 platform provides the full gamut of technical indicators and makes coding with them as easy as stacking together config objects. We are excited for you to experience it when creating your own strategies. Please share any comments or questions in the Community section of the app.

- Dan Salmo
Alta5, Platform Architect


Strategy.define({
    name: 'Enter Low BBand Trend',
    description: 'If price crosses lower BBand, buy after N points into an upswing. Exit is inverse of entry',
    positionType: 'Equity',
    target: 'SPY',

    enterAction: Trade.BUY,
    exitAction: Trade.SELL,

    closeTime: -15,

    params: [{
        id: 'points',
        type: 'number',
        label: 'Points from MA',
        min: 0,
        max: 5,
        required: true,
        allowDecimals: true,
        defaultValue: .04,
        step: .01,
        description: 'After the moving average bottoms, this is how many points to wait for it to rise before entering position. Also used inversely for exit.'
    }, {
        id: 'stoploss',
        type: 'percent',
        label: 'Stop Loss',
        description: 'The percentage a position is allowed to lose before being exited.',
        required: true,
        allowDecimals: true,
        decimalPrecision: 2,
        defaultValue: 1
    }, {
        id: 'devup',
        type: 'number',
        label: 'BBands SD Up',
        min: 0,
        max: 5,
        required: true,
        allowDecimals: true,
        defaultValue: 2,
        step: .01,
        group: 'Chart Settings',
        description: 'Bollinger Bands Standard Deviation Up'
    }, {
        id: 'devdn',
        type: 'number',
        label: 'BBands SD Down',
        min: 0,
        max: 5,
        required: true,
        allowDecimals: true,
        defaultValue: 2,
        step: .01,
        description: 'Bollinger Bands Standard Deviation Down'
    }, {
        id: 'bbperiod',
        type: 'number',
        label: 'BBands Time Period',
        min: 0,
        max: 100,
        required: true,
        allowDecimals: false,
        defaultValue: 5,
        step: 1,
        description: 'Bollinger Bands Time Period'
    }, {
        id: 'maperiod',
        type: 'number',
        label: 'Moving Average Time Period',
        min: 0,
        max: 100,
        required: true,
        allowDecimals: false,
        defaultValue: 30,
        step: 1,
        description: 'Moving Average Time Period'
    }],

    workers: [{
        id: 'todayChart',
        type: 'ichart',
        security: '$target',
        interval: Chart.Interval.MINUTE,
        minBars: 20,
        indicators: [{
            type: 'bbands',
            timePeriod: '$bbperiod',
            nbDevUp: '$devup',
            nbDevDn: '$devdn',
            maType: Chart.SMA
        }, {
            type: 'sma',
            timePeriod: '$maperiod'
        }]
    }],

    onMarketOpen: function(){
        delete this.data.status;  // start fresh daily
    },

    onSeek: function(target, data){

        var chart = data.todayChart,
            points = data.points,
            lbar = chart.lastBar,
            lband = lbar.indicator('bbands', 'lowerBand'),
            ma = lbar.indicator('sma'),
            status = data.status,
            malow = data.malow;

        // crossed lowerband, reset
        if(lbar.low < lband){
            data.status = 'wait-for-ma-bottom';
            data.malow = ma;

            // wait for ma to bottom
        } else if(status === 'wait-for-ma-bottom'){
            if(ma < malow){
                data.malow = ma;
            } else {
                data.status = 'wait-for-ma-rise';
            }

            // wait for rise
        } else if(status === 'wait-for-ma-rise'){
            var signal = malow + points;
            if(ma > signal){
                var memo = 'Buy: Price crossed Lower Bband, then MA:{0}, MA Low:{1}'.format(ma, signal - points);
                this.enter(target, memo);
                delete data.status;
            }
        }
    },

    onManage: function(target, position, data){

        var chart = data.todayChart,
            points = data.points,
            lbar = chart.lastBar,
            uband = lbar.indicator('bbands', 'upperBand'),
            ma = lbar.indicator('sma'),
            status = data.status,
            mahigh = data.mahigh,
            gain = position.gain * 100;

        // stoploss
        if(gain <= -data.stoploss){
            return this.exit(position, 'Stop loss reached - {0}%'.format(gain));
        }

        // crossed upperband, reset
        if(lbar.high > uband){
            data.status = 'wait-for-ma-peak';
            data.mahigh = ma;

            // wait for ma to peak
        } else if(status === 'wait-for-ma-peak'){
            var high = mahigh;
            if(ma > high){
                data.mahigh = ma;
            } else {
                data.status = 'wait-for-ma-drop';
            }

            // wait for drop
        } else if(status === 'wait-for-ma-drop'){
            var signal = mahigh - points;
            if(ma < signal){
                var memo = 'Sell: Price crossed Upper Bband, then MA:{0}, MA High:{1}'.format(ma, signal + points);
                this.exit(position, memo);
                delete data.status;
            }
        }
    }
});