Indicator with multiple calculations

Blocks have a limitation in TSLab, a block can only produce one value. There are situations when one indicator must calculate several values and all of them must be used in the script.

For example, we want to write a Bollinger indicator, and in the script we want to use the upper line, lower line and channel width. To calculate these values, it is enough to calculate the average SMA line and the standard deviation StDev.

Let's write a universal MyBollinger block, which, depending on the parameters, can issue the top line, bottom line, or the width of the Bollinger. The block will have the following parameters:

  • Period is the indicator calculation period.

  • Coef is the indicator calculation coefficient

  • Type is an enumeration type that specifies how the indicator is calculated. If Top, then displays the top line. If Bottom, then displays the bottom line. If Width, then displays the width of the channel.

The formulas are: Upper line: SMA + StDev * Coef. Bottom line: SMA - StDev * Coef. Channel width: top line - bottom line.

We will calculate the SMA and StDev indicators through caching (Context.GetData). This will reduce the number of identical calculations. For example, let's add one block for the top line, one block for the bottom line, and one block for the channel width to the script. When the script is executed, the first block will calculate the SMA, StDev values and write them to the cache. Then the second and third blocks will no longer calculate these values, they will simply take them from the cache.

Important: you need to cache indicators with a full chain of dependent parameters (more details here). For this, the INeedVariableParentNames interface has been added, it has one VariableParentNames property, which contains the names of parent blocks with parameters. We will add this property to all caches of calculated indicators in order to accurately identify the incoming IList<double> values.

using System.Collections.Generic;
using System.ComponentModel;
using TSLab.Script.Handlers;
using TSLab.Script.Helpers;

namespace MyLib
{
    [HandlerCategory("MyLib")]
    [HandlerName("MyBollinger")]
    [Description("Рассчитывает верхнюю, нижнюю линию или ширину боллинжера")]
    [InputsCount(1)]
    [Input(0, TemplateTypes.DOUBLE)]
    [OutputsCount(1)]
    [OutputType(TemplateTypes.DOUBLE)]
    public class MyBollinger : IStreamHandler, IContextUses, INeedVariableParentNames
    {
        public enum MyBollingerType
        {
            Top,
            Bottom,
            Width
        }

        [HandlerParameter(true, "20", Min = "1", Max = "100", Step = "1")]
        public int Period { get; set; }

        [HandlerParameter(true, "2", Min = "1", Max = "4", Step = "0.1")]
        public double Coef { get; set; }

        [HandlerParameter(true, nameof(MyBollingerType.Top))]
        public MyBollingerType Type { get; set; }

        public IContext Context { get; set; }

        /// <summary>
        /// Тут будет список наименований родительских блоков с параметрами. В виде строки.
        /// Нужно для правильного кеширования индикаторов SMA и StDev.
        /// </summary>
        public string VariableParentNames { get; set; }

        public IList<double> Execute(IList<double> values)
        {
            // Получить скользящую среднюю (SMA)
            var sma = Context.GetData("SMA", new[] { VariableParentNames, Period.ToString() },
                () => GetSma(Context, values, Period));

            // Получить стандартное отклонение (StDev)
            var stdev = Context.GetData("StDev", new[] { VariableParentNames, Period.ToString() },
                () => GetStDev(Context, values, Period));

            var array = Context.GetArray<double>(values.Count);
            switch (Type)
            {
                case MyBollingerType.Top:
                    // Если считаем верхнюю линию боллинжера, то складываем: SMA + StDev * Coef.
                    for (int i = 0; i < values.Count; i++)
                        array[i] = sma[i] + stdev[i] * Coef;
                    break;

                case MyBollingerType.Bottom:
                    // Если считаем нижнюю линию боллинжера, то вычитаем: SMA - StDev * Coef.
                    for (int i = 0; i < values.Count; i++)
                        array[i] = sma[i] - stdev[i] * Coef;
                    break;

                case MyBollingerType.Width:
                    // Если считаем ширину канала, то из верхней линии боллинжера вычитаем нижнюю.
                    for (int i = 0; i < values.Count; i++)
                        array[i] = (sma[i] + stdev[i] * Coef) - (sma[i] - stdev[i] * Coef);
                    break;
            }
            return array;
        }

        public static IList<double> GetSma(IContext ctx, IList<double> values, int period)
        {
            // Выводим в лог сколько раз вызвался этот метод при расчете скрипта.
            // Для одного боллинжера с одинаковыми параметрами будет только одни запуск этого метода.
            // Это только для демонстрации, в 'боевых' индикаторах вывод в лог не обязателен.
            ctx.Log($"Get sma", toMessageWindow: true);
            return Series.SMA(values, period, ctx);
        }

        public static IList<double> GetStDev(IContext ctx, IList<double> values, int period)
        {
            // Выводим в лог сколько раз вызвался этот метод при расчете скрипта.
            // Для одного боллинжера с одинаковыми параметрами будет только одни запуск этого метода.
            // Это только для демонстрации, в 'боевых' индикаторах вывод в лог не обязателен.
            ctx.Log($"Get stdev", toMessageWindow: true);
            return Series.StDev(values, period, ctx);
        }
    }
}

Last updated