Создание индикатора RVI

Пишем индикатор на примере индекса относительной бодрости (relative vigor index).

Этот урок составлен для новичков, однако, он подразумевает что у читателя уже присутствуют базовые знания языка Java Script и понимание основных принципов объектно ориентированного программирования (ООП). Для этого урока особенно важно понимание работы классов и наследования. Более детально об этом можно прочесть тут - developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

Для того чтобы создать индикатор для терминала Mobius Trader достаточно войти в терминал, нажать на кнопку `Индикаторы` и выбрать пункт меню `Редактор mCode`:

В появившемся окне нужно нажать на кнопку `Создать индикатор`:

Ввести название индикатора - `Relative_Vigor_Index`

И нажать на кнопку `Добавить`. После этого действия терминал создаст пустой файл-шаблон, с которым мы и будем работать. Если после этого действия вы не увидели редактор кода, то найдите ваш индикатор в верхнем левом углу и щелкните по нему мышкой. В этот момент вы должны увидеть такой шаблон:

Код в шаблоне со строки номер 2, где мы видим три ключевых слова — `export`, `class` и `extends`.

Слово `class` обозначает что мы создаем новый класс с названием Main. Слово `extends` обозначает что наш класс будет расширять уже существующий класс `Indicator`, и что он получит доступ ко всем родительским функциям. Файл с классом в языке JavaScript обычно называется `модуль`. Без модулей вероятность коллизий была бы велика — например, если бы два разработчика в одной программе объявили бы по переменной с названием `myVariable`, то в памяти программы сохранилась бы только та, что была создана последней. Благодаря модулям такая ситуация исключена, поскольку модуль это изолированный участок кода.

И последнее ключевое слово во второй строке это `export`. Как было сказано, модуль это изолированный участок кода. Поэтому, для того чтобы использовать модуль мы должны сперва экспортировать его значение. Другой человек сможет использовать написанный нами модуль при помощи ключевых слов `import` и `from`. Более детально с модульной структурой в языке JavaScript можно ознакомиться тут -

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

Вернёмся к разработке индикатора.

Формула вычисления значений индикатора известна, однако, нам не нужно пытаться воспроизвести её самим. Формула уже реализована, и нам нужно только вызвать соответствующую функцию. Все реализованные формулы находятся внутри класса `Indicators`. Нас интересует функция RVI - (mtrader7.com/ru/docs/Terminal/mScript/JavaScript/v1/Indicators/RVI)

Вызывать ее мы можем следующи образом:

await Indicators.RVI(symbol, timeframe, period, priceType, mode)

Как видно из документации функции RVI, мы должны иметь 5 входных параметров.

`symbol` и `timeframe` будут доступны «по умолчанию», поскольку `symbol` это текущая валютная пара, а `timeframe` это текущий выбранный временной период. Параметр `mode` не является обязательным, и мы не станем его использовать.

Таким образом, нам нужно позаботиться только о параметрах period и priceType. Мы знаем, что при создании объекта (экземпляра класса) будет вызван конструктор класса. Эта функция будет вызвана только один раз, и она будет вызвана раньше всего остального, ведь именно в этой функции будет происходить первоначальная “сборка” или рождение объекта (в нашем случае, индикатора).

Поэтому, в конструктор класса мы добавим следующие строчки кода:

this.addInput("ExtRVIPeriod", InputType.int, 10);

this.addInput("PriceType", InputType.PriceType, PriceType.Bid);

Мы могли бы просто записать значения для индикатора в два поля таким образом:

this.ExtRVIPeriod = 10;

this.PriceType =  PriceType.Bid

Но в этом случае эти параметры были бы навсегда `вшиты` в индикатор. Используя функцию `this.addInput` мы создаём два поля, которые можно будет изменить в течении работы нашего индикатора. Проверим как это работает – в правом верхнем углу включаем тумблер `Добавить в “Мои индикаторы` и жмем на кнопку `Сохранить` :

Теперь возвращаемся в терминал, в меню “Индикаторы” ищем пункт `Пользовательские` и там выбираем наш индикатор – Relative_Vigor_Index:

После этого мы увидим что входные параметры уже могут контролироваться пользователем:

Поскольку индикатор не готов, нет сымсла добавлять его на график. Поэтому просто нажимаем на кнопку `Закрыть` и возвращаемся в редактор кода. Поскольку мы делаем осциллятор, то мы должны указать что наш индикатор не должен накладываться на график цен, а должен быть отображён в отдельном окне. Это можно сделать следующей строчкой кода –

this.setProperty(Property.SeparateWindow, true);

Которую мы добавим в конструктор класса.

Поскольку для отображения данных нам нужно где-то хранить результаты вычислений функции `Indicators.RVI`, нам понадобиться создать два хранилища данных –

this.buffers = {
    ExtRVIBuffer : this.addBuffer(),
    ExtRVISignalBuffer : this.addBuffer(),
};

Хранилища данных это достаточно сложные объекты, более детально прочитать о них можно здесь - mtrader7.com/ru/docs/Terminal/mScript/JavaScript/v1/Buffer

Внимательные читатели заметили, что в коде конструктора уже присутствует хранилище данных, которое было создано автоматически. Нам оно не понадобиться, и мы удалим его.

На этом код функции конструктора будет завершён, и он должен выглядеть примерно так:

Как уже было сказано ранее, хранилище данных, или буфер, это сложный объект. Эти хранилища будут содержать информацию, которая будет представлена в графическом виде в терминале. Для этого хранилища нужно настроить. Делается это внутри функции onInit, которая будет вызвана сразу после того как индикатор будет помещён на график цен, и каждый раз, после того как пользователь изменит входные параметры (или настройки) индикатора. Функция будет иметь следующий вид:

Остановимся на этом шаге, сохраним индикатор и вернемся к графику цен. Снова добавим индикатор на график –

Как мы видим, окно с настройками изменилось. Теперь мы можем контролировать цвета и тип линий будущего индикатора. Эти данные берутся из настроек буферов, которые мы задали внутри функции `onInit`.

Теперь мы переходим к ключевому алгоритму индикатора — к функции onUpdate. Наконец-то наш код примет законченный вид!

Как видно из документации класса Indicator, функция onUpdate будет вызвана всякий раз, когда меняется график цен (когда на график цен приходит новый пипс). Таким образом, наш индикатор будет всегда соответствовать графику цен. Итак, посмотрим что происходит внутри кода.

Первое что бросается в глаза — ключевое слово async. Это слово означает что наш метод асинхронный. Останавливаться на этом не станем, поскольку различия между синхронными и асинхронными методами очень большие, и заслуживают отдельной темы. Более детально про async можно прочитать здесь - developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

Теперь мы возвращаемся к графику цен, и снова добавляем индикатор. В окне настроек нажимаем на кнопку `Ok`. В этот раз, мы увидим готовый индикатор под графиком цен:

Как видно, ничего сложного в примере нет. Все математические функции уже реализованы, и для программирования индикаторов для Mobius Trader 6 достаточно только набить руку, и запомнить, как пользоваться ключевыми классами и функциями, которые описаны в спецификации - mtrader7.com/ru/docs/Terminal/mScript/JavaScript/v1

Полный код индикатора относительной бодрости со всеми коментариями доступен ниже:

/**
 * Индикатор Relative_Vigor_Index (Индекс Относительной Бодрости)
 *
 * @module Relative_Vigor_Index
 * @see module:Indicator
 */
export class Main extends Indicator {

    /**
     * Конструктор класса.
     * Вызывается один раз при создании экземпляра класса для каждого экземпляра.
     * Не принимает никаких аргументов, неявно возвращает созданный экземпляр.
     *
     * @see Buffer
     * @see mScrip::addBuffer
     */
    constructor() {
        //Класс `Indicator` объявлен родительским классом для `Relative_Vigor_Index`.
        //Для корректной работы наследуемых функций необходимо проинициализировать родительский класс
        //Для этого необходимо вызвать родительский конструктор:
        super();

        //Поскольку индикатор `Relative_Vigor_Index` является осцилятором, то
        //нам необходимо отобразить его в отдельном окне, а не накладывать на график цены.
        //Для этого мы присваиваем свойству `SeparateWindow` значение `true`.
        this.setProperty(Property.SeparateWindow, true);

        //Задаем входной параметр для периода индикатора `ExtRVIPeriod`
        //Этот параметр будет использоваться по умолчанию, но он может быть переопределен пользователем.
        //Значение параметра `ExtRVIPeriod` будет равено 10
        this.addInput("ExtRVIPeriod", InputType.int, 10);

        //Задаем входной параметр для цены, которая будет использоваться при вычислениях.
        //Этот параметр будет использоваться по умолчанию, но он может быть переопределен пользователем.
        //Выбранная нами цена - Bid, или цена спроса (продажи).
        //Может быть `PriceType.Bid` или `PriceType.Ask`
        this.addInput("PriceType", InputType.PriceType, PriceType.Bid);

        //Для корректной работы индикатора нам понадобяться два хранилища для данных (буфера).
        //Хранилища данных это экземпляры класса `Buffer`
        //Оба хранилища мы поместим в поле данного объекта `this.buffers`.
        //В буфере `ExtRVIBuffer` будут храниться данные для отображения основной линии индикатора
        //В буфере `ExtRVISignalBuffer` будут храниться данные для отображения сигнальной линии индикатора
        //Метод `this.addBuffer()` не описан в данном классе, но он присутствует в классе mScript.
        //Поскольку `Relative_Vigor_Index` наследуется от `Indicator`,
        //а `Indicator` наследуется от `mScript`, мы можем его вызвать
        this.buffers = {
            ExtRVIBuffer : this.addBuffer(),
            ExtRVISignalBuffer : this.addBuffer(),
        };
    }

    /**
     * Метод onInit
     * Вызывается в момент размeщения индикатора на график цены и каждый раз при изменении входных параметров.
     * Не принимает никаких аргументов
     * Ничего не возвращает
     *
     * @see mScript::getInputs
     *
     * @see Buffer::setShape
     * @see Buffer::setColor
     * @see Buffer::setLabel
     * @see Buffer::setShortName
     * @see Buffer::setDrawBegin
     *
     * @override
     * @return void
     */
    onInit() {
        //Вызываем метод `this.getInputs()` для того чтобы получить входные параметры.
        //Метод `this.getInputs` не описан в данном классе, но он присутствует в классе `mScript`.
        //Поскольку `Relative_Vigor_Index` наследуется от класса `Indicator`,
        //а `Indicator` наследуется от mScript, мы можем его вызвать
        //Используем декомпозицию объектов для того чтобы создать локальную константу `ExtRVIPeriod`
        const  {ExtRVIPeriod} = this.getInputs();


        //Задаем настройки для отображения основной линии индикатора `Relative_Vigor_Index`
        //Он будет отображать линию - Shape.Line
        //Зеленого цвета - Color.Green
        //Эта линия будет подписана - "RVI"
        //Также у нее будет краткое название "RVI(${период индикатора})"
        //Отрисовка линии начнется 13 свечей (баров) назад от текущего момента.
        //Последнее значение может меняться в зависимости от настроек пользователя.
        this.buffers.ExtRVIBuffer
            .setShape(Shape.Line)
            .setColor(Color.Green)
            .setLabel("RVI")
            .setShortName("RVI(" + ExtRVIPeriod + ")")
            .setDrawBegin(ExtRVIPeriod + 3)
        ;


        //Задаем настройки для отображения сигнальной линии индикатора `Relative_Vigor_Index`
        //Он будет отображать линию - Shape.Line
        //Красного цвета - Color.Red
        //Эта линия будет подписана - "RVIS"
        //Также у нее будет краткое название "RVI(${период индикатора})"
        //Отрисовка линии начнется 17 свечей (баров) назад от текущего момента.
        //Последнее значение может меняться в зависимости от настроек пользователя.
        this.buffers.ExtRVISignalBuffer
            .setShape(Shape.Line)
            .setColor(Color.Red)
            .setLabel("RVIS")
            .setShortName("RVI(" + ExtRVIPeriod + ")")
            .setDrawBegin(ExtRVIPeriod + 7)
        ;
    }

    /**
     * Асинхронный метод onUpdate.
     * Переопределяет родительский метод из класса Indicator.
     * Этот метод будет вызываться каждый раз, когда меняется цена на графике.
     *
     * @see Indicators::RVI
     * @see Buffer::fill
     *
     * @override
     * @return {Promise.}
     */
    async onUpdate() {
        //Вызываем метод `this.getInputs` для того чтобы получить входные параметры.
        //Метод `this.getInputs` не описан в данном классе, но он присутствует в классе `mScript`.
        //Поскольку `Relative_Vigor_Index` наследуется от `Indicator`,
        //а `Indicator` наследуется от `mScript`, мы можем его вызвать.
        //Используем декомпозицию объектов для того чтобы создать локальные константы `ExtRVIPeriod`,
        //PriceType, ExtRVIBuffer и ExtRVISignalBuffer
        const  {ExtRVIPeriod, PriceType} = this.getInputs(),
            {ExtRVIBuffer, ExtRVISignalBuffer} = this.buffers;

        //Вызываем асинхронный метод Indicators.RVI.
        //Ключевое слово `await` означает, что скрипт остановит выполнение данной процедуры, пока не получит
        //результат из Indicators.RVI. Когда результат будет получен, он будет присвоен локальной переменной `buffers`
        let buffers = await Indicators.RVI(
            Current.Symbol,
            Current.TimeFrame,
            ExtRVIPeriod,
            PriceType
        );

        //В случае, если на нашем графике меньше баров, чем того требует функция `Indicators.RVI`
        //значение `buffers` будет равняться `null`, и на этом выполнение текущй процедуры закончится
        if (buffers) {
            //Иначе переменная `buffers` получит нужные нам значения для отображения индикаторов.
            //В этом случае мы передаем данные параметры в наши хранилища данных:
            ExtRVIBuffer.fill(buffers[IndicatorLine.Main]);
            ExtRVISignalBuffer.fill(buffers[IndicatorLine.Signal]);
            //В этот момент терминал отрисует основную и сигнальную линию индикатора `Relative_Vigor_Index`.
        }
    }
}