Инструменты пользователя

Инструменты сайта


atf:руководство

Собственный индикатор шаг за шагом

Примечание: Изложенный в этом руководстве материал одинаково применим как для торгового терминала Transaq® Trader, так и для тренажёра Transaq® Intra.

Базовые принципы

Базовым объектом с исходными данными для любого индикатора технического анализа или МТС1) является график изменения цены анализируемого инструмента во времени. Не случайно подпункт Скрипты ATF… расположен в меню Графики.

Массив line[], функция calc()

Основа любого индикатора — это функция calc(), которая содержит формулу для расчёта индикатора. Результат работы индикатора отображается на графике путём присвоения значений массиву line[].

Пример №1: Минимально допустимый индикатор.

function calc()
{
    line[0] = 0;
}

Приведенный выше код — работающий индикатор из одной линии, каждое значение которой равно нулю.

Программирование индикатора внутри функции calc() сводится к определению значений массива line[] (линий индикатора) в каждый момент времени графика по историческим данным или данным в реальном времени.

Следующий простейший пример показывает индикатор «канала», в два раза более широкого чем размер свечи2) за тот же период.

Пример №2: Индикатор удвоенного канала.

function calc()
{
    line[0] = high + (high - low) / 2;
    line[1] = low - (high - low) / 2;
}

Зарезервированные слова high и low обозначают максимальное и минимальное значения цен текущей свечи.

Разберёмся с тем, как ATF вычисляет и отображает такой индикатор. Для каждой, начиная со самой старой, свечки на графике выполняется функция calc(), в которой задаются значения массивов line[n], соответствующие различным линиями индикатора. Индикатор может содержать до 10 линий, обозначающиеся от line[0] до line[9]. Явно определять используемое количество линий не требуется — ATF сам определит, сколько линий задействовано в индикаторе.
Важно: Если по нумерации какая-либо линия пропущена, то ATF заполнит соответствующий ей массив нулями.

Пример №3а: Простой индикатор канала (некорректное определение).

function calc()
{
    line[0] = high;
    line[3] = low;
}

Хотя в индикаторе задано всего две линии line[0] и line[3], ATF будет считать, что в индикаторе имеются линии line[1] и line[2], значения которых всегда равны нулю. Иначе говоря, приведенный выше пример эквивалентен следующему:

Пример №3б: Простой индикатор канала (корректное определение).

function calc()
{
    line[0] = high;
    line[1] = 0;
    line[2] = 0;
    line[3] = low;
}

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

Пример №3в: Простой индикатор канала (корректное и оптимальное определение).

function calc()
{
    line[0] = high;
    line[1] = low;
}

Рассмотрим процесс расчета индикатора удвоенного канала (Пример №2) по шагам:

  1. ATF вызывает функцию calc() для первой свечи, заполняя значения line[0] и line[1] с использованием high и low, соответствующих этой свече.
  2. Для второй и для всех оставшихся свечей операция повторяется.
  3. Когда весь индикатор по историческим данным рассчитан, ATF ожидает информации о новых сделках по инструменту в реальном времени. При каждой новой сделке он снова вызывает значение calc() для последней свечи, пересчитывая значения линий.

Рассмотрим пример другого индикатора. Допустим, что средний объём сделки на рынке способен помочь нам предсказать будущее поведение цены, так как, возможно, он говорит о том, насколько крупные инвесторы торгуют в данный момент. Большую часть времени средний объём сделки невелик, однако, если он вдруг резко увеличивается, это может свидетельствовать о том, что более состоятельные инвесторы начинают закупать или распродавать свои бумаги, что потенциально указывает на скорую смену текущего движения. Для отслеживания таких ситуаций мы можем написать следующий индикатор:

Пример №4: Индикатор удельного объёма сделок (FishSize).

// FishSize
function calc()
{
    line[0] = volume / trades;
}

Если ввести индикатор именно так, то ATF может выводить сообщения об ошибке деления на ноль. Это случится потому, что при расчёте индикатора по историческим данным ATF может попытаться рассчитать значение индикатора за период, когда не было совершено сделок (trades). Как избежать деления на ноль — будет рассмотрено далее в разделе Контроль выполнения, переменные, области видимости, однако уже сейчас этот индикатор пригоден к использованию3).

Сдвиги и управление границами отображения

На практике индикаторов, использующих значения только текущей свечи, практически не существует. Почти все индикаторы рассчитываются с использованием как минимум некоторой истории, а часто – и с использованием предыдущих своих окончательных и промежуточных значений. Для ссылки на значение за границами текущей свечки к используемому параметру свечи (high, low, close, volume и т.д.) необходимо добавить его сдвиг относительно текущей свечи в квадратных скобках. Например, для линии EMA формула будет выглядеть следующим образом:

line[0] = 0.9 * line[0][-1] + 0.1 * (close - line[0][-1]);

Мы здесь поставили фиксированное значение параметра alpha = 0.1, поскольку пока еще не изучили работу с переменными — это упущение будет исправлено в следующем разделе.

В качестве примера сдвига параметров свечей можно привести индикатор Momentum:

line[0] = close - close[-10];

Однако, если просто задать формулу для EMA или Momentum в приведенном виде, то это приведет к ошибкам. При вызове функции calc() в случае EMA TRANSAQ попытается обратиться к линии line[-1] при расчете самого первого значения свечки, то есть будет использовать значение линии для свечи, которой не существует. При попытке построения такого индикатора TRANSAQ выведет сообщение «ссылка на неопределенную линию индикатора».

В случае с индикатором Momentum произойдет аналогичная ошибка: при вызове функции calc() для первых девяти свечек (счет свечей начинается с нулевой по старой программистской традиции) будет совершена попытка обратиться к свечами, которые находятся за пределами допустимой истории. Для устранения данных ошибок необходимо указать свечи, для которых не надо выполнять функцию calc(). Приведем полный пример индикаторов EMA и Momentum в корректном виде, а после объясню значение используемых функций:

// EMA
function init()
{
    setInitCandles(1);
    line[0] = close;
}
 
function calc()
{
    line[0] = 0.9 * line[0][-1] + 0.1 * close;
}
// Momentum
function init()
{
    setInitCandles(10);
}
 
function calc()
{
    line[0] = close - close[-10];
}

Как видно в приведенном примере выше, в дополнение к calc() мы определили еще одну функцию init(). Эта функция, если она определена пользователем, вызывается один раз перед тем как TRANSAQ начнет вызывать calc() для каждой свечки. Она оказывается полезной для выполнения подготовительных действий перед расчетом индикатора, и в частности для определения свечек, для которых не нужно вызывать calc(). Все значения индикатора, которые не были явно заданы пользователем (для Momentum первые десять) считаются равными нулю. В случае с EMA внутри функции init() задается начальное значение индикатора.

Смысл используемой функции setInitCandles(10) прозрачен — она указывает на начальное количество свечек, для которых не надо рассчитывать индикатор с помощью функции calc(). Причины для использования этой функции могут быть различными — необходимость функции calc() некоторого количества истории (как в случае с Momentum), необходимость задать начальное значение по отличной от calc() формуле, и другие возможные причины, которые будут рассмотрены в последующих разделах. Надо заметить, что при ссылке на параметры свечей внутри функции init() предполагается, что мы вызываем ее для самой первой свечки. Выражение line[0] = close; подставит close первой свечки для графика. Так же как и с любыми параметрами свечей, в функции init() можно ссылаться не только на текущую свечу, но и на свечи с некоторым сдвигом относительно текущей. Здесь проявляется различие между функциями calc() и init(). Так как функция init() вызывается относительно самой первой свечи, сдвиг параметров относительно нее не может быть отрицательным - это приведет к обращению к недоступным данным.

В случае же функции calc() сдвиг относительно текущей свечи не может быть положительным, так как при расчете параметров для последней свечи на графике при положительном сдвиге будет произведена попытка обратиться к данным, которых еще нет:

function init()
{
    line[0] = close - close[-1]; // Не правильно - функция init вызывается
                                              // для самого первой свечи и значения close[-1]
                                              // не существует
    line[0] = close[1] - close; // А вот это уже вполне легальная операция
}
 
function calc()
{
    line[0] = close - close[-1]; // Легальная операция, если в функции init() была вызвана
                                              // функция setInitCandles(1);
    line[0] = close[1] - close; // При расчете последнего значения close[1] еще не определен,
                                             // и данное выражение приведет к ошибке
}

Приведенное правило сдвигов относительно свечей может показаться странным, однако оно продиктовано объективными соображениями. Если вдруг возникнет необходимость ссылаться в calc() на свечи с положительным сдвигом — это говорит лишь о том, что в формуле расчета индикатора имеется ошибка.

В индикаторе Momentum, как мы его запрограммировали, имеется один недостаток: первые десять его значений равны нулю, хотя правильнее было бы их вообще не отображать, так как реально они не были рассчитаны. В случае с индикатором Momentum это не критично, однако, например, в случае со скользящими средними, требующими для своего расчета некоторую историю, подобный недочет привел бы к резкому скачку графика от нуля до значения цены в начале истории, что ломает масштаб окна. Для задания границ отрисовки индикатора следует использовать функцию setBounds(line, begin, end):

function init()
{
    setInitCandles(10);
    setBounds(0, 10, 0);
}
 
function calc()
{
    line[0] = close - close[-10];
}

Первый параметр функции setBounds определяет номер линии, для которой мы задаем границы, второй — левую границу индикатора, третий — правую. Если бы мы задали правую границу индикатора отрицательной, то он отображался бы на графике не до конца. Если задать правую границу положительным числом, то мы получим возможность определить дополнительные значения индикатора, которые будут выглядывать за массив свечей, как, например, в индикаторах Alligator и Ichimoku Kinko Hyo. Для задания этих дополнительных значений следует использовать положительный сдвиг относительно текущей линии. Нетривиальный пример, не критичный для дальнейшего изложения:

function init()
{
    setBounds(0, 10, 10);
}
 
function calc()
{
    line[0][10] = close;
    line[1] = close - line[0];
}

Данный индикатор отображает две линии. Первая — это значение close, сдвинутое вперед на десять баров. Вторая - разница между текущим значением close и текущим значением первой линии. Таким образом, если включить режим «сдвиг свечей», первая линия будет вылезать на десять свечей вправо. Вторая же линия могла бы быть рассчитана проще:

line[1] = close - close[-10];

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

Контроль выполнения, переменные, области видимости

Под контролем выполнения понимаются конструкции, позволяющие изменить последовательный ход выполнения программы в зависимости от определенных условий. Вспомним пример индикатора FishSize. При образовании новой свечи, когда еще не прошло сделок, выражение
line[0] = volume / trades;
приводило к делению на ноль. Понятно, что в случае равенства нулю параметра trades, нам нельзя применять данную формулу. Решить это можно с помощью оператора if:

function calc()
{
    if (trades != 0) {
        line[0] = volume / trades;
    }
    else {
        line[0] = 0;
    }
}

Оператор «!=» означает сравнение на неравенство. Смысл приведенного выше кода, думаю, нет необходимости объяснять подробно. Если условие внутри круглых скобок после оператора if выполняется, то будет вычислена первая формула. В противном случае выполняется формула после оператора else.

В общем виде синтаксис оператора if выглядит следующим образом:

if (expr1) { code1 }
else if (expr2) { code2 }
else if (expr3) { code3 }
else { code4 }

Здесь выражения expr должны быть любыми логическими выражениями (смотрите соответствующий раздел документации), а code — соответствующий код, который необходимо выполнить, если соответствующее условие верно. Проверяются условия подряд, и выполняется только первая ветка кода, для которой условие выполнилось. После этого дальнейшие условия не проверяются. Последний code4 будет выполнен лишь в том случае, если ни одного условия не было выполнено. Веток else if может быть сколько угодно много, либо не быть вообще. Ветка else может либо быть одна, либо не быть вовсе. Это все достаточно общие вещи для большинства языков программирования, поэтому я не останавливаюсь на этом подробно.

Второй оператор контроля выполнения — цикл while. Его синтаксис прост:

while (expr) {code}

Указанный в фигурных скобках код будет выполняться до тех пор, пока условие expr истинно.

Здесь самое время поговорить о переменных. Определяются они с помощью ключевого слова var. Приведем пример:

//SMA
var period = 15;
 
function init()
{
    setInitCandles(period - 1);
    setBounds(0, period - 1, 0);
}
 
function calc()
{
    var i = 0;
    while (i < period) {
        line[0] += close[-i];
        i += 1;
    }
    line[0] /= period;
}

В самом начале объявляется глобальная переменная period, равная 15. Функция init() задает границы индикатора (первые period − 1 значений не могут быть рассчитаны по причине нехватки данных). Функция calc() вычисляет сумму параметров close[i], начиная от close[0] и заканчивая close[period − 1], после чего делит полученный результат на period, получая среднее арифметическое, которое и равняется индикатору SMA.

Надо сказать, что использование циклов в функции calc() — не лучшая идея. Поскольку цикл выполняется для каждой свечи и каждой сделки в отдельности, использование большого числа индикаторов с циклами при большом количестве построенных графиков может привести к снижению производительности. Не использовать циклы, конечно, во многих случаях не получится, да и в большинстве ситуаций они не приведут к серьезным потерям в скорости, однако все же следует помнить о том, что если можно написать индикатор одинаково просто либо с циклом, либо без — лучше выбрать последнее.

В приведенной реализации индикатора SMA объявлено две переменные: period и i. Первая переменная объявлена перед всеми функциями, вторая — внутри функции calc(). Вследствие этого переменная period будет доступна из всех функций, и она будет сохранять свое значение при каждом последующем обращении к calc(). Переменная же i локальна для calc() и объявлена внутри нее. Ее значение будет задаваться заново при каждом последующем входе в функцию calc().

TRANSAQ поддерживает полноценные области видимости, ограниченные фигурными скобками:

function calc()
{
    var x = 1;
    {
        var x = 2;
        var y = 3;
        line[0] = x; // здесь x равен 2
    }
    line[1] = x; // здесь уже используется x, определенный в самом начале
                       // значение x, определенное внутри фигурных скобок больше
                       // не существует в данной области видимости
    line[2] = y; // ошибка: переменная y перестала существовать сразу после выхода
                       // за блок фигурных скобок
}

Помимо уже перечисленных способов контроля выполнения, существуют дополнительные менее важные ключевые слова, описание которых можно найти в документации. Еще один способ задать переменные — это объявить их внешними, то есть настраиваемыми из диалога. Приведем пример на рассмотренном ранее EMA:

// EMA
extern period = 14;
 
function init()
{
    setInitCandles(1);
    line[0] = close;
}
 
function calc()
{
    var alpha = 2 / (period + 1);
    line[0] = alpha * close + (1 - alpha) * line[0][-1];
}

Для этого индикатора период уже будет задаваться в диалоге настройки параметров индикатора при добавлении его на график. Указанный мной период 14 — это просто значение по умолчанию, которое можно переопределить из диалога индикатора, не прикасаясь к исходному коду.

Промежуточные индикаторы

Примерно половина всех индикаторов не обходится без использования промежуточных скользящих средних и среднеквадратичного отклонения. Простейший пример индикатора, который использует и то и другое — Bollinger Bands:

// Bollinger Bands
extern period = 14;
extern k = 2;
 
function calc()
{
    line[0] = MovAvg(ind_ema, period, pt_close);
    var sd = StdDev(stddev_abs, period, pt_close);
    line[1] = line[0] + k * sd;
    line[2] = line[0] - k * sd;
}

Синтаксис прост и понятен. Первый параметр уточняет тип индикатора (смотрите в документации), второй — период, третий — что именно сглаживать. Третьим параметром должен идти либо тип цены, либо какая-то линия.

С периодом скользящей средней связан тонкий нюанс: он не должен меняться от свечки к свечке. То есть следующая конструкция не сработает:

var period = 10;
 
function calc()
{
    line[0] = MovAvg(ind_ema, period, pt_close);
    period += 1;
}

При попытке построить такой индикатор будет выведена ошибка, так как при построении каждой новой свечки период будет меняться. Условие неизменности периода от свечки к свечке в действительности не накладывает никаких ограничений — в реальности такой необходимости не должно возникать.

В качестве дополнительного примера рассмотрим возможную реализацию индикатора MACD:

line[0] = MovAvg(ind_ema, period1, pt_close) -
		MovAvg(ind_ema, period2, pt_close);
line[1] = MovAvg(ind_ema, period3, line[0]);

По аналогии со ссылками на свечки и линии, к скользящим средним можно обращаться со сдвигом следующим образом:

line[1] = MovAvg(ind_ema, period, line[0])[-1];

В этом случае необходимо задать setInitCandles(1), так как при расчете первой свечи конструкция [-1] будет ссылаться на свечу с отрицательным номером.

Для ссылки на все остальные индикаторы, в ATF используется функиция IndRef, первым параметром которой должно быть указано строковое значение идентификатора индикатора. Гистограмму MACD, например, можно описать следующим образом:

function calc()
{
	line[0] = IndRef("macd", period1, period2, period3,
				ind_ema, ind_ema, ind_ema, pt_close)[0] -
			IndRef("macd", period1, period2, period3,
				ind_ema, ind_ema, ind_ema, pt_close)[1];
}

Для доступа к индикатору необходимо перечислить все его параметры. После функции IndRef() в квадратных скобках необходимо указать номер линии, которая нас интересует, начиная с нулевой. При необходимости можно сослаться на индикатор со сдвигом. Так, например, можно подсчитать скорость изменения OBV:

function calc()
{
	line[0] = IndRef("obv")[0] - IndRef("obv")[0][-period];
}

Ссылаться таким же образом на индикаторы Trades, Volume, MA и Standard Deviation нельзя. Для первых двух следует использовать ссылки trades и volume соответственно, для MA функцию MovAvg, для Standard Deviation — функцию StdDev.

Настройка отображения

В этом разделе рассматриваются возможности интерпретатора, которые не сказываются непосредственно на расчет индикатора, но задают параметры его отображения.

В TRANSAQ по умолчанию линии индикатора отображаются черным цветом и рисуются сплошной линией. Это всегда можно изменить из диалога индикатора, но удобнее сразу определить цвет и стиль индикатора, которые будут использоваться по умолчанию с помощью макросов.

Макросы для задания отображения по умолчанию имеют простой формат:

#line n style color

Здесь n — номер линии начиная с нуля, style — стиль линии, color — цвет. В качестве цвета можно указать ключевое значение из списка в документации, либо задать его в шестнадцатеричном виде. Начинаться макросы должны с начала строки, появляться могут в любом месте, хотя лучше определять их в самом начале кода из чисто эстетических соображений. Например, добавление следующих макросов придет к тому, что первая линия будет сплошной красного цвета, а вторая — зеленая пунктирная:

#line 0 solid red
#line 1 dashed green

То же самое в шестнадцатеричном виде:

#line 0 solid #ff0000
#line 1 dashed #008000

Второй доступный макрос: #samewindow (указывается в начале строки). Он заставляет TRANSAQ по умолчанию добавлять индикатор в то же подокно, где находится график цены. Без этого макроса новые индикаторы строятся в отдельных подокнах.

Кроме того, могут оказаться полезными функции countCandles() и lackHistory(). Первая возвращает общее количество свечек, доступных на графиках, а вторая прекращает расчет индикатора и выводит сообщение об ошибке. Например, если мы пишем индикатор SMA, который требует для построения количество свечей на графике, не меньшее своего периода, правильным будет написать функцию init() следующим образом:

function init()
{
    if (countCandles() < period - 1) {lackHistory();}
    // Далее надо дописать уже знакомые setBounds и setInitCandles
}

Конструкция с вызовом lackHistory() проверяет доступное количество свечек на графике, и если их недостаточно, останавливает расчет индикатора, а пользователю показывается сообщение о недостатке истории. Большой необходимости в этой функции нет, так как даже без нее никаких ошибок не произойдет, однако ее применение в уже готовом отлаженном индикаторе позволяет информировать пользователя о недостатке истории. В противном случае индикатор просто не рассчитается, что без объявления причин может выглядеть весьма странно.

Определение функций

Пользователь может определять собственные функции в ATF с помощью ключевого слова function. Параметры функции должны быть определены после названия функции с использованием ключевого слова var. Пример функции для вычисления факториала (для значений меньших нуля в качестве результата будет возвращен ноль):

function factorial(var n)
{
    if (n == 0) {return 1;}
    if (n < 0) {return 0;}
    var res = n;
    while (n > 1) {
        n -= 1;
        res *= n;
    }
    return res;
}

Функции в ATF могут быть рекурсивными, то есть вызывать сами себя. Рассмотрим, например, функцию, вычисляющую числа Фибоначчи:

function f(var k)
{
    if (k < 1) {return 0;}
    if (k == 1 or k == 2) {return 1;}
    return (f(k - 1) + f(k - 2));
}

Рекурсии могут быть удобными в ряде случаев, но использования их стоит избегать, поскольку они достаточно дорогостоящи в вычислительном плане. Также следует воздерживаться от использования собственных функций, если есть аналогичная предопределенная альтернатива (например, для чисел Фибоначчи имеется замена fibon(N)).

Необходимо знать о том, что вы можете использовать лишь те функции, которые уже были определены где-то выше. Например, следующий пример выдаст ошибку:

function calc()
{
    line[0] = average(high, low, open, close);
}
 
function average(var a, var b, var c, var d)
{
    return (a + b + c + d) / 4;
}

Причина ошибки в том, что определение функции average находится после определения функции calc, и функция calc просто напросто «не знает» о существовании функции average, однако пытается ее использовать. Исправить ситуацию можно просто перенеся определение average перед определением функции calc:

function average(var a, var b, var c, var d)
{
    return (a + b + c + d) / 4;
}
 
function calc()
{
    line[0] = average(high, low, open, close);
}

Из этого правила следует, что ситуация, когда одна функция (f1), вызывает другую (f2), а f2 в свою очередь вызывает f1, недопустима в ATF. Это невозможно написать чисто синтаксически. В действительности с этим нет большой проблемы, так как появление таких сложных рекурсий почти всегда свидетельствует об ошибке и о том, что надо пересмотреть реализацию алгоритма.

Сигналы

Сигнал — это автоматическое уведомление пользователя о том, что произошло некоторое событие. В ATF сигналы являются частью индикаторов, то есть «чистых» сигналов, которые могли бы существовать отдельно от индикатора нет (впрочем, в будущем они обязательно появятся). В действительности это весьма удобно - всегда есть возможность не только получить уведомление о произошедшем событии, но и тут же увидеть его на графике (если именно наличие дополнительных линий является нежелательным, их всегда можно спрять с помощью макросов, либо из окна свойств индикатора). Рассмотрим простой пример сигнала, который подается при пересечении цены скользящей средней:

#samewindow
#line 0 solid red
 
extern period = 9;
 
function init()
{
	setInitCandles(1);
}
 
function calc()
{
	line[0] = MovAvg(ind_ema, period, pt_close);
 
	if (close >= line[0] and close[-1] < line[0][-1]) {
		signal::alert("Покупай!");
	}
 
	if (close <= line[0] and close[-1] > line[0][-1]) {
		signal::alert("Продавай!");
	}
}

Надо сказать, что данный сигнал не идеален — он сработает даже если реального пересечения не произойдет, а цена просто сравняется со значением скользящей средней. Альтернативный вариант — использовать вместо нестрогих неравенств (<=, >=) строгие (<, >), однако в этом случае возникает другая трудность — если закрытие свечи достигнет скользящей средней, но пересечет ее только лишь на следующей свечке, то условие close[-1] < line[0][-1] и аналогичное для пересечения сверху вниз не сработают, так как выполняется равенство close[-1] == line[0][-1]. Это можно исправить проверяя в цикле сразу ряд предыдущих переменных (плохое решение — трудоемкое, а также есть риск обратиться к несуществующей свече). Ситуацию можно разрешить, введя дополнительную переменную, которая будет запоминать текущий тренд (многоточиями обозначены места, которые не изменились):

...
var trend = 0;
// Условимся об обозначениях:
// 1 - бычий тренд
// -1 - медвежий тренд
...
 
function calc()
{
	...
	if (trend == 1 && close[-1] < line[0]) {
		signal::alert("Продавай!");
	}
 
	if (trend == -1 && close[-1] > line[0]) {
		signal::alert("Покупай!");
	}
 
	// Запоминаем текущее положение дел
	if (close > line[0]) {trend = 1;}
	else if (close < line[0]) {trend = -1;}
}

На практике такие тонкости мало влияют на поведение сигнала, и поэтому в большинстве случаев можно обойтись и первоначальным простым вариантом. Замечу, что signal::alert — это не единственный способ подать сигнал из ATF. Пользователю доступны следующие индикаторы:

signal::alert(string)Вывести сигнал в диалоговое окно
signal::notify(string1, string2)Вывести сообщение в таблицу «Уведомления» (доступна в меню «Таблицы»), где string1 — заголовок уведомления, string2 — содержимое уведомления. Можно вызывать данную функцию только с одним параметром.
signal::output(string)Вывести сообщение в окно вывода интерпретатора (в которое обычно выводятся сообщения об ошибках). Данная функция может быть наиболее удобна для отладки ваших индикаторов, когда вам нужно посмотреть чему равны отдельные переменные при вычислении индикатора, если что-то идет не так, как задумывалось. Кроме того, ее можно использовать для более удобного отслеживания рыночной информации в реальном времени.
signal::play(filepath)Проиграть указанный файл (при указании пути до файла необходимо заменять символ «\» на «\\» — в следующем разделе это объясняется подробнее).

Строки

Строки используются в основном для подачи сигналов. Естественно, что просто задать в сигнале фиксированную строку вроде «Покупай!» — явно не достаточно, так как хотелось бы как минимум указать какую именно бумагу покупать, а возможно и вывести некую дополнительную информацию. Рассмотрим, как это можно было бы сделать в нашем прошлом примере:

...
	signal::alert("Покупай " + getSecName() + "!");
...
	signal::alert("Продавай " + getSecName() + "!");

Функция getSecName() возвращает название инструмента, для которого строится индикатор или сигнал. Некоторым вероятно захочется использовать вместо названия бумаги ее идентификатор биржи. Его можно также получить с помощью функции getISIN(). Кроме того, ATF имеет ряд дополнительных функций для работы со строками — смотрите соответствующий раздел документации (например, вы можете получить название рынка на котором торгуется бумага). Оператор «+», как видно из примера, осуществляет конкатенацию (то есть «склеивание») строк.

Если внутри строки необходимо использовать двойные кавычки, то для этого нужно использовать специальный символ «\»:

signal::alert("ATF is better than "Pyre""); // Не верно!
signal::alert("ATF is better that \"Pyre\""); // Верно.

В первом случае ATF увидит только строку от первой до второй кавычки: «ATF is better than », а «Pyre» распознает как неизвестный идентификатор. Во втором случае кавычки «спрятаны» за конструкцией «\»», так что он не распознает их как конец строки. Сам символ «\» имеет специальный смысл — он вставляет символ, следующий за ним в строку, когда последний имеет специальный смысл, которого надо избежать, как в случае с кавычкой, которая без «\» распознается как конец строки. Так как сам символ «\» тоже является специальным, то его тоже надо защищать, если есть необходимость использовать его в строке. Это существенно для указания пути до файла:

signal::play("c:\music\tada.wav"); // Не правильно!
signal::play("c:\\music\\tada.wav"); // Правильно.

В первом случае в функцию signal::play() будет передана строка «c:musictada.wav», поскольку «\» интерпретируется не как часть строки, а как специальный символ, который «защищает» символ, следующий за ним от неправильной интерпретации. Постоянное использование «\\» для директорий может показаться неудобным, но это старая программистская традиция, которой следует большинство современных языков. Так получилось.

К строкам можно применять не только операции склеивания — их также можно запоминать в переменных и передавать в виде параметров функциям. Рассмотрим, как можно было бы изменить наш прошлый пример (хотя это и не имеет особого смысла, кроме, пожалуй, минимального прироста производительности):

...
var title = getSecName();
...
	... {
		var message = "Покупай " + title;
		message += "!";
		signal::alert(message);
	}
 
	... {
		var message = "Продавай " + title;
		message += "!";
		signal::alert(message);
	}

При необходимости строки можно передавать в арифметические функции:

var x = "2";
x += "5";
// Поскольку мы задаем значения в кавычках, ATF распознает их как строки
// и использует не арифметическое сложение, а конкатенацию строк.
// Таким образом здесь x оказывается равен строке "25".
 
x = sqrt(x);
// Теперь x равен 5, даже несмотря на то, что в sqrt мы передали x, который
// ранее интерпретировался как строка.

Последняя инструкция в примере оказалась возможной, потому что ATF автоматически приводит строки к числам и обратно, когда это необходимо, и любые переменные «на лету» меняют свой тип. Замечу, что лучше не мешать строки с числами и избегать ситуаций, которые я изобразил выше. Тем не менее, иногда они могут оказаться полезными, как, например, в следующей переработке примера со скользящей средней:

...
var title;
 
function init() {
	title = getSecName();
}
 
function calc() {
	var message; // Здесь переменная имеет нулевое значение
 
	if (...) {
		message = "Покупай " + title + "!";
	}
 
	if (...) {
		message = "Продавай " + title + "!";
	}
 
	if (message) { // Проверяем наличие сообщения
		signal::alert(message);
	}
}

В последнем примере if приводит значение message к логическому типу. Если переменная message содержит значение 0 или пустую строку, то считается, что условие if не выполняется. Если message содержит нечто, отличное от нуля, либо какую-либо непустую строку, то условие считается выполненным. Рассмотрим следующий пример:

x = "2"; // x - строка
y = 2; // y - число
z = x + y; // Чему равен z?
z = "abc" + y; // z = "abc2";

Данный пример сложен тем, что невозможно однозначно сказать, как должен повести себя ATF. Должен ли он интерпретировать «+» как арифметическую операцию и рассматривать строку «2» как число? Или он должен интерпретировать число 2 как строку и выполнить конкатенацию? В данном конкретном примере ATF выполнит арифметическую операцию, однако это поведение может совершенно неожиданно измениться при самых, казалось бы незначительных изменениях кода. ATF не дает никаких гарантий на выполнение подобных операций, к тому же если сегодня он выполняет сложение, то завтра это поведение может измениться. Обратите внимание, что во втором случае конфликта не возникает: «abc» можно интерпретировать только как строку, поэтому такой код сработает. Явно специфицировать, как надо интерпретировать значения, можно следующим образом:

x = "2";
y = 2;
z = as_number(x) + y; // z = 4;
z = x + as_string(y); // z = "22";

Функции as_number и as_string заставляют ATF интерпретировать их параметры как числа или как строки соответственно.

Может также возникнуть потребность сделать extern-переменную строковой. По умолчанию это невозможно, так как extern-переменные должны быть числовыми по мнению диалога свойств индикатора. Указать явно, что нам требуется именно extern-строка, можно следующим образом:

extern "string" a = "Hello"; // extern-строка
extern "number" b = 1; // extern-число
extern c = 2; // так же extern-число по умолчанию

Взаимодействие calc() с глобальными переменными; многократные сигналы

Исключительно в демонстрационных целях предположим в этом разделе, что нам не доступна функция MovAvg, и мы решили реализовать ее самостоятельно. Первый способ сделать это — задействовать циклы, где на каждый вызов calc() честно рассчитывается среднее арифметическое последних N свечей. Как уже упоминалось выше, это плохой способ, так как расчет циклов чаще всего оказывается весьма трудоемким. Правильнее было бы запоминать сумму последних N значений во временной переменной (можно для этого использовать само значение line, но для демонстрационных целей нам понадобится все же переменная), и потом для каждой новой свечи использовать это запомненное значение, предварительно удаляя из него самый старый элемент, и добавляя новый:

#samewindow
 
extern period = 14;
var sum = 0;
 
function init()
{
	if (countCandles() < period) {lackHistory();}
	var i = 0;
	while (i < period) {
		sum += close[i];
		i += 1;
	}
	setInitCandles(period);
	setBounds(0, period, 0);
}
 
function calc()
{
	sum += close;
	sum -= close[-period];
	line[0] = sum / period;
}

Важно понимать, что calc() вызывается при каждом новом трейде, то есть на каждую свечку calc() будет вызван многократно. При этом казалось бы к переменной sum будет прибавлен один и тот же close и вычтен один и тот же close[-period] многократно, что должно привести к некорректной работе индикатора. Однако это не так: ATF понимает, что при приходе нового трейда индикатор должен пересчитать свое последнее значение заново. По этой причине ATF запоминает последнее состояние глобальных переменных, и каждый раз при вызове calc() восстанавливает те значения, которые были актуальны для предыдущей свечки. В противном же случае, это приводило бы к неожиданному поведению индикаторов, зависящему от количества трейдов в свече.

Аналогичная ситуация имеется и с сигналами. Посмотрим еще раз на первоначальный код сигнала, который подается при пересечении ценой скользящей средней:

...
	if (...) { // Произошло пересечение вверх
		signal::alert(...);
	}
 
	if (...) { // Произошло пересечение вниз
		signal::alert(...);
	}

По каждому новому вызову calc() должен вызываться signal::alert (причем даже в случае с дополнительной переменной trend, так как она тоже при каждом calc будет восстанавливаться в свое последнее значение, соответствующее завершенной свечке). Но этого ли мы ожидали, когда писали сигнал? Цена пересекла линию SMA, ATF об этом сообщил, и второй раз для нового трейда сообщать тоже самое уже не требуется, до тех пор пока не случится новое пересечение. ATF корректно обрабатывает и эту ситуацию, вызывая каждый сигнал лишь единожды для каждой свечи, и пользователь получает желаемый результат.

Такое «необычное» поведение ATF оправдано в большинстве случаев, однако оно также говорит о том, что мы не можем подать более одного сигнала (или провести более одной сделки) на каждую свечу, и мы не можем отслеживать собственно количество сделок. Если все же такая потребность возникнет, ATF содержит необходимые инструменты.

Во-первых, это multipe-версии для сигналов: signal::alertMultiple, signal::notifyMultiple, signal::playMultiple, signal::outputMultiple. Они полностью аналогичны уже рассмотренным ранее функциям за тем лишь исключением, что для них не работает механизм «не более одного сигнала на свечку». Если заменить в нашем сигнале со скользящей средней signal::alert на signal::alertMultiple, то после пересечения ценой скользящей средней мы будем получать сообщение после каждого трейда, до тех пор, пока не начнется новая свеча.

Во-вторых, имеется возможность определить «статичные» переменные (ключевое слово static). Они ведут себя в полной аналогии с глобальными переменными, за тем лишь исключением, что они не восстанавливают свои последнии значения при каждом вызове calc(). Это свойство может использоваться для создания достаточно изощренных индикаторов и торговых стратегий. Чуть ниже я покажу один из таких примеров.

Третий инструмент, позволяющий обойти стандартное поведение ATF — это определяемая пользователем функция onNewCandle(). Она вызывается однократно после того, как на графике появляется новая свечка. Надо заметить, что вызывается она только после вызова calc(). Определение данной функции может оказаться полезным не только чтобы обойти стандартное поведение ATF, но и просто для не слишком агрессивных стратегий, когда сделки совершаются не непосредственно в момент срабатывания индикатора, а с некоторой задержкой.

Чтобы изложенное стало более понятным, приведем несколько примеров. Предположим, что нам не доступны обычные функции signal, а доступны лишь их multiple-аналоги. Нам необходимо сэмулировать поведение «обычного» сигнала, который срабатывает единожды. Это может быть реализовано следующим образом:

static alerted;
 
function alert(var string) {
	if (not alerted) {
		signal::alertMultiple(string);
		alerted = true;
	}
}
 
function onNewCandle() {
	alerted = false;
}
 
...
	// Следующий alert будет работать
	// точно как signal::alert
	alert(string);
...

Принцип работы функции alert прозрачен: при вызове сигнала устанавливается статическая переменная alerted, которая в дальнейшем не даст сигналу срабатывать. Сбросится она лишь внутри функции onNewCandle(), когда начнет отрисовываться новая свечка. Переменная alerted обязана быть статичной, иначе ее значение сбрасывалось бы при каждом новом вызове calc().

Рассмотрим пример посложнее. Следующий сигнал срабатывает, если цена совершает значительные колебания внутри свечки:

// Signal "local volatility"
// by Dobrovensky Roman
 
#samewindow
#line 0 nodraw
 
extern percent = 10;
extern multiplier = 1;
extern incrMultiplier = 1.1;
 
static accumulated = 0;
static lastclose;
static lasttrades;
static maximum;
 
function increaseVolatility()
{
	signal::alertMultiple("По бумаге " +
	                      getSecName() +
				          " сильно возрасла волатильность.");
 
	maximum *= incrMultiplier;
}
 
function init()
{
	maximum = percent;
	line[0] = 0; // В любом случае необходимо определенить
	             // хотя бы одну линию, даже если это
				 // "чистый" сигнал
}
 
function onNewCandle()
{
	accumulated = 0;
	lastclose = close;
}
 
function calc()
{
	accumulated += multiplier * (trades - lasttrades) * abs(lastclose - close);
	lastclose = close;
	lasttrades = trades;
	if (accumulated > close * maximum / 100) {
		increaseVolatility();
	}
}

Сигнал приведен здесь в демонстрационных целях. Принцип его работы следующий: каждый раз как вызывается функция calc(), мы подсчитываем изменение цены, которое внесла эта сделка. Мы используем статические переменные, так как я накапливаю информацию о сделках внутри свечи. Теоретически, при очень высокой активности на рынке, calc() может вызываться реже, чем для каждой сделки ввиду высокой нагрузки, поэтому мы дополнительно умножаем изменение цены на количество трейдов, которое произошло с момента прошлого вызова calc() (это число должно быть единицей). Если накопленное изменение цены превышает текущую цену более чем на percent процентов (эта величина задается пользователем), срабатывает сигнал, и текущее значение накопленного изменения цены в дальнейшем будет сравниваться уже с большей величиной.

Механическая торговля и контроль портфеля

Механическая торговля ничем принципиально не отличается от подачи сигналов. Существует четыре функции для выставления заявки на рынок: trade_action::buy, trade_action::sell, trade_action::buyMultiple, trade_action::sellMultiple. Данные функции выставляют заявки на рынок. Отличие multiple-функций от обычных такое же, как и в случае сигналов. Каждая из этих функций может принимать от двух до трех параметров. Первый параметр указывает объем сделки (положительное число), второй параметр указывает в каких величинах измеряется объем сделки. Возможны три значения: ::money, ::lots и ::securities, которые обозначают деньги, лоты и отдельные бумаги соответственно. Третий параметр указывает цену. Если его не задать, то сделка будет совершена по рыночной цене (это на данный момент не работает для опционов FORTS). Рассмотрим стратегию, основанную на двух скользящих средних:

#samewindow
#line 0 solid red
 
extern period = 9;
extern amount = 10000;
 
function init()
{
	setInitCandles(1);
}
 
 
function calc()
{
	line[0] = MovAvg(ind_ema, period, pt_close);
 
	if (close >= line[0] and close[-1] < line[0][-1]) {
		trade_action::buy(amount, ::money);
	}
 
	if (close <= line[0] and close[-1] > line[0][-1]) {
		trade_action::sell(amount, ::money);
	}
}

Данная стратегия будет продавать и покупать выбранный инструмент при каждом пересечении ценой скользящей средней. Совершаться сделка будет на сумму, задаваемую во внешней переменной amount, которая по умолчанию равна 10000 рублей (если в эту сумму не укладывается целое количество лотов, то сделка будет округлена в меньшую сторону). Как видно, код для торговли по стратегии отличается от кода для подачи сигнала только в месте совершения действия: раньше это был signal::alert, а теперь trade_aсtion::buy. Оптимальный подход к написанию торговых стратегий — вначале написать систему сигналов, чтобы система подавала правильные сигналы в нужные моменты времени, и корректировать их для механической торговли.

Можно совершать сделки не только на какой-то фиксированный объем, но и, например, исходя из количества доступных средств или из количества уже приобретенных бумаг. Для этого служат функции getMoneyBalance() и getSecBalance(). Все перечисленные функции, включая ввод заявок, действуют для выбранного в данный момент клиента. Если надо совершить сделки от имени разных клиентов, то этого можно достичь с помощью функций getClient() и setClient(). Приведем бесполезный с практической точки зрения пример с комментариями, который покажет, как можно работать с данными функциями:

signal::alert("Текущий клиент:" + getClient());
// Выводим имя текущего клиента

if (getSecBalance() > 100) {
    // Если клиент имеет более ста бумаг, для которых
	// рассчитывается индикатор, продаем сто.
	trade_action::sell(100, :securities:);
}

setClient("ИВАНОВ");
// Меняем клиента на ИВАНОВА

if (getMoneyBalance() « 10000) {
	// Если Иванов имеет менее 10000 рублей, сообщаем об ошибке
	signal::alert("У Иванова недостаточно средств!");
}
else {
	// В противном случае покупаем
	trade_action::buy(10000, ::money);
}

setClient("ПЕТРОВ");
// Опять меняем клиента на Петрова
trade_action::buy(getMoneyBalance() / 2, ::money);
// Покупаем текущей бумаги на половину свободных средств

Сделка в ATF совершается по текущему инструменту, то есть по инструменту, для которого был вызван скрипт.

Если указать для торговой операции цену (третий параметр), то сделка будет проведена только по указанной цене, а не по рыночной (для рынка FORTS это единственный режим работы через ATF). При этом заявка будет действительна до тех пор, пока ее не снять. Для этого служат три функции: trade_action::cancelBuyOrders(), trade_action::cancelSellOrders() и trade_action::cancelAllOrders(), которые снимают соответственно все заявки на покупку, все заявки на продажу и любые заявки по данной бумаге соответственно. Снять заявки по отдельной бумаге или снять лишь часть заявок нельзя.

При желании можно так же выставлять более сложные заявки, в том числе заявки по разным инструментам, об этом читайте в параграфе 12, где рассказывается об объектах в ATF.

Объекты: Файлы, массивы, хэши, сделки, ордера, буфферы линий

Начиная с версии ATF 1.6 имеется ряд предопределенных объектов: массивы, хэши, файлы и буфферы линий. Синтаксис работы с предопределенными объектами я продемонстрирую на сдедующем простом примере который экспортирует данные из индикатора в файл:

extern "string" filename = "export.txt";
var f;
 
function init()
{
	f = new_object("file");
	f.wopen(filename);
}
 
function calc()
{
	line[0] = MovAvg(ind_ema, 9, pt_close);
	f.writeLn(line[0]);
}

В функции init выполняется инициализация файла. Сначала с помощью функции new_object создается объект типа «file» и ссылка на файл записывается в переменную f. Следующей строчкой файл открывается на запись.

Если в переменной находится ссылка на какой-либо объект, то функции этого объекта вызываются с помощью оператора «точка», которой отделяется имя функции. В данном случае первой функцией является wopen, которая открывает файл с заданным именем на запись. Уже ниже в функции calc(), после каждой рыночной сделки вызывается фукнция writeLn, которая записывает строку текста в файл. Закрывать файл в ATF не обязательно — при удалении индикатора, пересчете или смене параметров он будет закрыт автоматически.

Следующий пример демонстрирует работу с массивами:

var data;
 
function init()
{
	data = new_object("array");
}
 
function calcAverage()
{
	var i = 0;
	var ave = 0;
	var n = data.size();
	while (i < n) {
		ave += data[i];
		i += 1;
	}
	ave /= n;
	return ave;
}
 
function onNewCandle()
{
	signal::output(calcAverage());
	data.clear();
}
 
 
function calc()
{
	line[0] = MovAvg(ind_ema, 9, pt_close);
	data.push(line[0]);
}

В этом примере каждый пересчет индикатора заносит новое значение в массив data, а при формировании новой свечи выводится среднее значение индикатора на свечу. Массив здесь в принципе не был необходимым (вполне можно обойтись и переменной static), но использовался для демонстрации.

Создаваемый объект на этот раз имеет тип «array». Метод push добавляет в конец массива элемент, метод size возвращает размер массива, clear очищает массив от всех элементов. Обратиться к элементам массива можно с помощью квадратных скобок, внутри которых указывается элемент массива.

Аналогично осуществляется работа с хэшами (ассоциативными массивами), но только ссылка осуществляется не по номеру, а по строке.

var data;
 
function init()
{
	data = new_object("hash");
}
 
function calc()
{
	data["high"] = high;
	data["low"] = low;
	data["ma"] = MovAvg(ind_ema, 9, pt_close);
	line[0] = data["ma"];
}

Пример совершенно беcсмыссленный, он просто демонстрирует синтаксис работы с хэшами. Практическое применение они находят при работе с информацией о заявках и сделаках. Следующий пример будет выводить цены всех сделок, прошедших по бумаге:

function onATFTrade(var id)
{
	var x = getTrade(id);
	signal::output("Робот совершил сделку по цене " + x["price"]);
}
 
function calc()
{
	line[0] = 0;
}

Здесь есть две новых конструкции. Первая - фукнция onATFTrade. Она автоматически вызывается каждый раз, как исполняется заявка, поставленная роботом по данному инструменту (если интересуют только сделки, которые были совершены вне данного робота, можно использовать onClientTrade). Этой функции в качестве аргумента передается биржевой номер сделки. Далее с помощью функции getTrade по номеру сделки можно получить хэш, в полях которого описаны параметры сделки. В данном случае мы используем параметр «price». В соответствующем разделе документации подробно описаны все данные, которые имеются при сделке.

Аналогичная ситуация и с заявками:

function onATFOrder(var id)
{
	signal::alert("Заявка выставлена! Номер: " + id);
}
 
 
function onATFOrderErr(var str)
{
	signal::alert(str);
}
 
function calc()
{
	line[0] = 0;
}

Еще один тип объекта — это «linebuffer». Данные объекты практически полностью дублируют поведение обычных лений line. Фактически они представляют собой массив, с тем лишь отличием, что элемент [0] указывает на элемент, соответствующий текущему номеру свечки, и вместо обычных методов массивов вроде size(), он автоматически принимает размер, соответствующий количеству свечей на графике (либо больше - вполне допустимо обращаться к элементам с положительным сдвигам без ограничений). В остальном можно использовать объекты linebuffer в полном соответствии с массивами line:

var buffer = 0;
var period = 10;
 
function init()
{
	buffer = new_object("linebuffer");
	setBounds(0, period, 0);
}
 
function calc()
{
	buffer[0] = (open + high + low + close) / 4;
 
	if (noCandle() > period) {
		line[0] = buffer[0] - buffer[-period];
	}
}

В этом примере мы вместо использования buffer[0] могли бы использовать и просто line[0], однако тогда эта промежуточная линия вывелась бы на экран (если не использовать макрос nodraw), что может быть нежелательно, учитывая в особенности то, что линий line может быть не более десяти. Буфферы linebuffer ничем в этом смысле не ограничены.

Стоит отдельно пояснить присвоение значения buffer[0]. Дело в том, что когда вы пишете например такой код:

x = 123;

То то что находится в переменной x целиком затирается и туда помещается просто числовое значение 123. Если до этой строчки в переменной x находился какой-то объект (файл, массив, linebuffer, xml-документ - неважно), после присвоения переменной числа этот объект уничтожается и в переменной оказывается просто число. Таким образом следующий код например не имеет смысла:

x = new_object("linebuffer");
x = 123;
x[-1] = 456;

В последней строчке переменная x уже не содержит объект типа linebuffer - это не более чем число 123. По этой причине правильнее будет писать так:

x = new_object("linebuffer");
x[0] = 123;
x[-1] = 456;

При использовании объектов ATF стоит так же иметь ввиду, что каждый вызов new_object резервирует дополнительную память, поэтому желательно хранить все объекты в глобальных переменных. В ATF работает простой механизм «сборки мусора», но его использование может быть не слишком эффективным, так что лучше подходить к созданию объектов с осмотрительностью, во всяком слчае на данном этапе развития Transaq.

Важным примером использования объектов в ATF, а вернее хэшей, является выставление заявки по хешу. Рассмотрим как это делается:

var order = new_object("hash");
order["price"] = 1.72;
order["quantity"] = 10;
order["operation"] = OP_BUY;
order["usecredit"] = true;
order["condition"] = COND_LAST_DOWN;
order["condvalue"] = 1.75;
trade_action::transact(order);

Здесь вначале создается хэш, а затем его значениями указываются параметры заявки. Чтобы эту заявку выставить, необходимо вызвать функцию trade_action::transact (в полной аналогии со всеми другими заявками имеется так же заявка trade_action::transactMultiple). Список полей, которые возможно заполнить для заявки и их значения, смотрите в разделах «Объекты» и «Константы».

Начиная с версии ATF 1.12 есть более удобный и простой способ сохранения данных в файлы: объект типа xmlarchive. Объекты этого типа представляют собой простой удобный интерфейс доступа к xml-файлам специального формата, в которых можно записывать данные переменных ATF

Объекты xmlarchive могут инициализироваться для чтения xml-файла или для записи в xml-файл. В первом случае после создания объекта (new_object(«xmlarchive»)) необходимо загрузить файл командой loadfile(filename), после чего можно считывать из него данные. Во втором случае вначале надо создать новый xml-файл командой newdocument(filename), записать в него данные, и созранить файл командой savefile(). Считывание и запись осуществляется командами savevar(name, value) и loadvar(name). Следующий простой пример демонстрирует как записать в файл данные при прекращении выполенения индикатора и считать при инициализации индикатора:

var balance;  // Просто некоторые переменные
var x;        // предположительно испольщуемые
var money;    // роботом, которые мы сохраним
 
function init()
{
	var xml = new_object("xmlarchive");
	xml.loadfile("file.xml");
	balance = xml.loadvar("balance");
	x = xml.loadvar("x");
	money = xml.loadvar("money");
}
 
function onStopIndicator(var reason) {
	var xml = new_object("xmlarchive");
	xml.newdocument("file.xml");
	xml.savevar("balance", balance);
	xml.savevar("x", x);
	xml.savevar("money", money);
	xml.savefile();
}

В примере выше все переменные оказались глобальными и мы задали им те же имена в XML-хранилище, что они имеют в программе. На самом деле можно сохранять произвольные данные под любыми именами (для имен можно использовать только латинский алфавит и цифры), в том числе в качестве сохраняемых переменных могут выстпать массивы и хеши - их сохранение ничем не отличается от обычных переменных. Однако если стоит цель сохранить именно глобальные переменные, то это можно сделать и проще, использовав функции saveglobals() и loadglobals():

function init()
{
	var xml = new_object("xmlarchive");
	xml.loadfile("file.xml");
	xml.loadglobals();
	x = xml.savevar("x");
	money = xml.savevar("money");
}
 
function onStopIndicator(var reason) {
	var xml = new_object("xmlarchive");
	xml.newdocument("file.xml");
	xml.saveglobals();
	xml.savefile();
}

Простые примеры объектов

В этом параграфе мы рассмотрим ряд простых прикладных примеров.

Для многих стратегий оказывается полезным сохранять данные между сессиями или запусками Transaq. Следующая программа, которую мы рассмотрим сейчас подробнее, считает количество вызовов функции функций calc() и onNewCandle(), и сохраняет эти параметры в файл, причем при следующем запуске скрипта счет продолжается от сохраненных значений:

static c1 = 0;
static c2 = 0;
 
var file;
 
function init()
{
	file = new_object("file");
	file.ropen("data.txt");
	if (file.isopen()) {
		c1 = file.readLn();
		c2 = file.readLn();
		file.close();
	}
	file.wopen("data.txt");
}
 
 
function onNewCandle()
{
	c2 += 1;
	file.seek(0);
	file.writeLn(c1);
	file.writeLn(c2);
}
 
function calc()
{
	if (isHistoryCalculated()) {
		c1 += 1;
	}
	line[0] = 0;
}

Переменные c1 и c2 считают вызовы calc() и onNewCandle() соответственно, их мы и будем сохранять в файл. В первой строчке будем сохранять значение c1, а во второй - c2.

В функции init() выполняются подготовительные действия: вначале открывается файл «data.txt» на чтение (ropen), и если он открылся (file.isopen()), то из файла считываются значения c1 и c2, после чего файл закрывается. В следующей строке файл открывается снова на запись. Старое содержимое при этом удаляется. Если его надо сохранить и дописывать данные в конец файла, то можно воспользоваться функцией waopen.

При начале новой свечи (onNewCandle) мы помимо увеличения счетчика c2 будем так же сохранять данные в файл. ATF на данный момент не имеет функций, срабатывающих например при выключении Transaq (они появятся в ближайшем будущем), поэтому на данный момент сохранение данных лучше проводить регулярно. Сам код сохранения данных крайне простой: вначале мы переходим в начало файла (seek(0)), чтобы переписать его, а затем командами writeLn последовательно записываем два значения. Замечу, что функция writeLn, в отличие от просто функции write, записывая в файл значение сразу переходит на новую строку.

Здесь так же следует обратить внимание на использование функции isHistoryCalculated(). Она проверяет вызывается ли функция calc() на исторических данных, или же рассчитывается по сделкам. Нам важно отсечь вызовы calc() на исторических свечках, так как в противном случае мы бы каждый раз после загрузки данных из файла прибавляли бы еще число вызовов calc() при рассчете истории.

Второй пример демонстрирует как сохранять в файл информацию о сделках. Саму стратегию мы добавлять не будем, а рассмотрим лишь код, который отвечает за сохранение данных.

var file;
 
function init()
{
	file = new_object("file");
	file.waopen("data.txt");
}
 
 
function saveTrade(var id)
{
	var trade = getTrade(id);
	if (trade["operation"] == OP_BUY) {
		file.write("BUY ");
	}
	else {
		file.write("SELL ");
	}
 
	file.write(trade["quantity"]);
	file.write(" lots at price ");
	file.writeLn(trade["price"]);
}
 
 
function onClientTrade(var id)
{
	file.write("Client order: ");
	saveTrade(id);
}
 
 
function onATFTrade(var id)
{
	file.write("ATF order: ");
	saveTrade(id);
}

Данный пример будет сохранять в файл информацию о всех сделках, совершаемых по данной бумаге пользователем. Сюда входят как сделки, совершаемые данным роботом, так и сделки, совершаемые другими роботами либо самим трейдером. При совершении сделки данным роботом ATF выполняет функцию onATFTrade, параметром которой передается идентификатор сделки. При выполнении всех остальных клиентских сделок по данной бумаге, вызывается onClientTrade. В нашем примере мы записываем в файл и те и другие сделки, помечая кем они выполнены («Client order» или «ATF order» соответственно). Вся информация о сделках совпадает в обоих случаях, поэтому мы задали единую функцию saveTrade(), которая по идентификатору сделки сохраняет подробности о ней в файл.

Само устройство функции saveTrade крайне простое: вначале мы получаем информацию о сделке в виде хеша используя функцию getTrade, а затем уже сохраняем интересующие нас данные в файл. Подробное описание этих полей может быть найдено в разделе «Объекты» документации.

Отдельно стоит заметить только просохранение типа операции. При работе с заявками в ATF используются константы (каждая из них на самом деле числовой идентификатор), но удобнее сохранять их в файл в текстовом виде. Поэтому мы сначала проверяем тип сделки (OP_BUY или нет), а затем уже записываем соответствующий данному типу операции текст.

Работа со стаканом (начиная с ATF 1.8)

Начиная с версии ATF 1.8 пользователю стала доступной работа со стаканом заявок, осуществляемая посредством объекта «book». Для начала работы необходимо создать объект «book» и осуществить подписку на данные по стакану с помощью функции subscribe:

var book;
 
function init()
{
	book = new_object("book");
	book.subscribe();
}

Для того, чтобы работать со стаканом, необходимо, чтобы данные по нему приходили от сервера. Команда subscribe как раз и осуществляет эту подписку, если подписки на данные еще нет. Когда данные от сервера получены, актуальное состояние стакана нужно загрузить с помощью команды load. Она загружает в память актуальные данные по стакану, которые далее можно использовать. Например, в следующей функции ATF выставляет заявку на покупку по цене, соответствующей второй лучшей на покупку:

function calc()
{
	book.load();
	var price = book.getBidPrice(1);
	trade_action::buy(1, ::lots, price);
}

После того, как данные загружены в объект с помощью команды load, с помощью комапд getBidPrice, getBidVolume, getAskPrice и getAskVolume можно получать цены и объемы заявок в стакане котировок. Номер, передаваемый в данные функции является позицией в стакане начиная от лучшей цены. Например, лучшая цена покупки — это getBidPrice(0), а объем, на который покупают по этой цене — это getBidVolume(0).

Проверить общее количество позиций в стакане (то есть цен, по которым есть заявки), можно с помощью функций getAskPosCount и getBidPosCount. Проверить просто наличие заявок в стакане можно с помощью команды isempty (она эквивалентна выражению getAskposCount() && getBidPosCount()). Здесь необходимо заметить, что ATF работает только с тем количеством позиций в стакане, которые Transaq получает от шлюза на стороне брокера, то есть из ATF доступны ровно те позиции, которые доступны в обычном стакане Transaq.

Отслеживание пересечения линий (ver. 1.13)

Один из наиболее распространенных видов сигналов — это сигналы пересечения линий индикатора. ATF предоставляет ряд простейших встроенных функций по отслеживанию этих пересечений. Первая функция, которая просто проверяет сам факт наличия пересечения - это функция isCross(a, b), которой в качестве параметра передаются номера линий, для которых необходимо установить факт пересечения. Эту функцию можно вызывать и для исторических данных, добавив третьим параметром номер свечи (считая от начала истории), либо вызвав предварительно функцию setCurrentPosition().

При этом подходе придется однако постоянно отслеживать наличие пересечения линий, например, в функции calc(). ATF может сам этим заняться и вызывать пользовательскую функцию в тот момент, когда происходит пересечение. Для того, чтобы ATF начал отслеживать пересечения линий, надо вызвать функцию addCrossWatch(), параметром для которой передается номер линии. Далее, когда будет происходить пересечение, ATF будет вызывать определенную пользователем функцию onCross() двумя параметрами которой будут выступать нормера линий, которые пересеклись (первый параметр — это номер линии, которая вышла вверх).

Для примера рассмотрим следующего робота, который открывает длинную позицию по индикатору MACD, а закрывает при пересечении ценой скользящей средней (на экране при этом отображается лишь индикатор MACD):

#line 2 nodraw
#line 3 nodraw
 
extern period1 = 12;
extern period2 = 26;
extern period3 = 9;
extern ma_period = 13;
 
function init()
{
	setInitCandles(1);
}
 
 
// Прекращение отслеживания  пересечения скользящей средней
// и начало отслеживания пересечения MACD
 
function startWatchingMACD()
{
	addCrossWatch(0);
	addCrossWatch(1);
	delCrossWatch(2);
	delCrossWatch(3);
}
 
 
// Прекращение отслеживания  пересечения MACD
// и начало отслеживания пересечения скользящей средней
 
function startWatchingMA()
{
	addCrossWatch(2);
	addCrossWatch(3);
	delCrossWatch(0);
	delCrossWatch(1);
}
 
function onHistoryCalculated()
{
	startWatchingMACD();
}
 
// Эта функция вызывается автоматически при пересечении линий
 
function onCross(var a, var b)
{
	// Если быстрая линия MACD пересекла медленную
	// снихзу вверх, а цена выше скользящей средней,
	// открываем позицию и начинаем отслеживать песечение
	// скользящей средней.
	if (a == 0 && line[3] < close) {
		trade_action::buy(1, ::lots);
		startWatchingMA();
	}
	// Если произошло пересечение ценой линии 
	if (a == 3) {
		trade_action::sell(1, ::lots);
		startWatchingMACD();
	}
}
 
function calc()
{
	line[0] = IndRef("macd", period1, period2, period3, ind_ema, ind_ema, ind_ema, pt_typical)[0];
	line[1] = IndRef("macd", period1, period2, period3, ind_ema, ind_ema, ind_ema, pt_typical)[1];
	line[2] = close;
	line[3] = MovAvg(ind_ema, ma_period, pt_close);
}

Основное действие происходит в функции onCross. Первый параметр показывает, какая линия при пересечении оказалась выше, а второй - какая ниже. При этом после каждого пересечения мы меняем список линий, которые отслеживаем. После того, как произошло пересечение по индикатору MACD, нас он больше не интересует до момента закрытия позиции — поэтому в функции startWatchingMA() мы перестаем отслеживать пересечение линий MACD, и включаем отслеживание скользящих средних. Это в частности гарантирует нам, что при повторном пересечении линий MACD, не будет совершена еще одна сделка: до тех пор, пока позиция не закроется, MACD будет игнорироваться роботом.

Допустим, что мы хотели бы закрывать позицию не при пересечении ценой скользящей средней, а на отскоке, когда она пересечет скользящую среднюю опять снизу. Все, что для этого надо — заменить условие

if (a == 3) {
...
}

на условие

if (a == 2) {
...
}

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

Полезно иметь ввиду, что отслеживание пересечения линий — довольно трудоемкая задача для ATF. На каждую рыночную сделку ATF проверяет наличие пересечения после вызова функции calc(), и соответственно чем больше линий отслеживается, тем это более затратно. Таким образом addCrossWatch() и delCrossWatch() помимо того что выполняют чисто логическую фонукцию включения и выключения отслеживания переключений, они так же весьма сильно сказываются на быстродействие работы ATF, хотя какой-то эффект может быть заметен лишь при большом числе работающих роботов и большой частоте сделок.

В примере выше сразу после пересечения линий мы перестаем отслеживать их состояние, так как в приведенном алгоритме это не нужно. Однако может случиться так, что это пересечение при пересчете индикатора по очереденой сделке вдруг исчезнет и при дальнейшем рассчете индикатора оно уже будет отсутствовать. ATF позволяет отслеживать это. Как только пересечение исчезает после очередной сделки, вызывается функция onFalseCross(), которой в качестве двух аргументов передаются опять же номера линий. Если после вызова onFalseCross(), пересечение опять возникнет, то опять будет выполнена функция onCross().

Если рассматривать этот алгоритм подробнее, то происходит следующее: как только происходит пересечение отслеживаемых линий по сделке, ATF запоминает это и функция onCross() срабатывает единожды: то есть по следующим сделкам, хотя функция calc() вызывается, а пересеченеие остается видно на графике, onCross() уже не вызывается. Если же пересечение вдруг исчезает, то срабатывает функция onFalseCross() и далее ATF считает, что пересечения не было, и если вдруг после очередного вызова calc() пересечение появится, то onCross() снова отработает.

Непосредственно отслеживать момент пересечения по сделке необходимо не в каждой стратегии — часто требуется лишь факт пересечения скользящих средних на уже полностью сформировавшейся свече. В этой ситуации вмето функции onCross() правильнее использовать функцию onApprovedCross() — в целом принцип ее работы такой же, но вызывается она только после отработки функции onNewCandle(). Здесь надо подчеркнуть, что если в коде программы определена только функция onApprovedCross(), но нет функции onCross(), то проверка пересечений по каждому трейду не будет проводиться вообще, и соответствено выполнение программы будет значительно менее трудоемким. Но это опять же может быть замечено лишь при большом количестве индикаторов по высоколиквидным инструментами. В обычной практике с этим вопросом трейдеру вряд ли придется столкнуться.

Функция isCross(), о которой было упомянуто в самом начале, является самой низкоуровневой и именно она используется при отслеждивания пересечений для функций onCross() и onApprovedCross().

Окружение ATF (ver. 1.12)

Начиная с версии 1.12 у скриптов ATF появилась возможность обмениваться информацией друг с другом — для этого используется так называемое «окружение ATF» — фактически глобальный хеш, доступный из всех скриптов, а так же система глобальных событий, которые могут быть сгенерированы любым скриптом или же непосредственно пользователем, и по которым в каждом из скриптов будет выполнена соответствующая функция.

Работа с окружением ATF сводится к диалогу «окружение ATF», который доступен из меню «Графики», и к следующим четырем функциям:

setEnvVariable(key, value)Устанавливает переменную окружения с именем key в значение value (если такой переменной нет — создает ее).
getEnvVariable(key)Считывает переменную окружения с именем key.
postEnvEvent(string)Генерирует событие окружения с параметром string.
onEnvEvent(string)Переопределяется пользователем и вызывается по любому событию окружения. В качестве параметра передается параметр string, который был передан в функицю postEnvEvent().

Генерировать события а так же управлять переменными можно так же и из диалога «Окружение ATF». Далее мы рассмотрим несколько простейших примеров, которые могут продемонстрировать как пользоваться данными функциями.

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

static tradingallowed = 0;
 
function onEnvEvent(var string)
{
	if (string == "turnrobotson") {
		tradingallowed = true;
	}
	else if (string == "turnrobotsdown") {
		tradingallowed = false;
	}
}
 
function buy(/*параметры*/) {
	if (tradingallowed) {
		trade_action::buy(/*параметры*/);
	}
}
 
function sell(/*параметры*/) {
	if (tradingallowed) {
		trade_action::sell(/*параметры*/)
	}
}

Теперь мы можем включать и выключать всех роботов по соответствующей команде из диалога «окружение ATF». Это не особо полезная функция, так как то же самое можно сделать и по соответствующему пункту меню. Больший смысл это приобретает, когда мы хотим отключать или включать роботов по какому-то сигналу от других роботов. Например, можно останавливать все индикаторы, как только какой-то индекс (например, индекс РТС) пересекает свою скользящую среднию сверху вниз. Тогда код индикатора, который ставится на индекс РТС должен будет выглядеть следующим образом:

#samewindow
#line 0 nodraw
 
extern period = 13;
 
function init()
{
	addCrossWatch(0);
	addCrossWatch(1);
	setInitCandles(1);
}
 
function onCross(var a, var b)
{
	if (a == 1) {
		postEnvEvent("turnrobotsdown");
		delCrossWatch(0);
		delCrossWatch(1);
		// Отключаем отслеживание,
		// чтобы не напрягать систему
	}
}
 
function calc()
{
	line[0] = close;
	line[1] = MovAvg(ind_ema, period, pt_close);
}

Можно сделать аналогичный прием, чтобы торговля была разрешена, когда цена выше скользящей средней, и запрещена, когда ниже. Можно поступить таким же образом через события, а можно задействовать глобальные переменные, что в этом случае будет даже удобнее, так как не придется задумываться о начальном состоянии переменной tradingallowed в роботах. В этом случае код роботов будет содержать такие функции:

function buy(/*параметры*/) {
	if (getEnvVariable("tradingallowed")) {
		trade_action::buy(/*параметры*/);
	}
}
 
function sell(/*параметры*/) {
	if (getEnvVariable("tradingallowed")) {
		trade_action::sell(/*параметры*/);
	}
}

Код индикатора для индекса будет таким:

#samewindow
#line 0 nodraw
 
extern period;
 
function init()
{
	setInitCandles(1);
}
 
function calc()
{
	line[0] = close;
	line[1] = MovAvg(ind_ema, period, pt_close);
 
	if (line[0] > line[1]) {
		setEnvVariable("tradingallowed", 1);
	}
	else {
		setEnvVariable("tradingallowed", 0);
	}
}

Окружение ATF можно использовать и для обмена более содержательными данными между скриптами. Следующий пример показывает, как можно написать скрипт торговли, который будет совершать сделки, когда относительное отклонение бумаги превысит в какое-то количество раз относительное отклонение индекса от закрытия прошлого дня. Причем шоритить можно только в случае падения индекса, а длинную позицию открывать только в случае роста индекса. Следующий индикатор будет устанавливать отклонение индекса в переменную окружения:

static initprice;
 
function init()
{
	// Вычисляем номер последней свечки за прошлый торговый день
	var candleno = getCandleByTime(getTimeObject(0, 0));
	if (candleno == -1) {
		lackHistory();
		// Ошибка: не загружено достаточное количество данных
	}
	initprice = close[candleno];
}
 
function calc()
{
	setEnvVariable("deriv", 100 * close / initprice);
	// Для наглядности отклонение будем измерять в процентах
}

А это непосреднственно торговый скрипт по бумагам:

static initprice;
 
// Расхождение с индексом, при
// котором открываем позицию
extern rate_openpos = 1.5;
 
// Расхождение с индексом, при
// котором закрываем позицию
extern rate_closepos = 0.7;
 
// position показывает нашу позу
// 0 - отсутствие позиции робота
// 1 - лонг
// -1 - шорт
static position = 0;
 
function init()
{
	var candleno = getCandleByTime(getTimeObject(0, 0));
	if (candleno == -1) {
		lackHistory();
	}
	initprice = close[candleno];
}
 
function calc()
{
	var deriv = getEnvVariable("deriv");
	var k = (100 * close / initprice) - deriv;
	// Посчитали отставание/опережение бумаги относительно индекса.
 
	// Если позиция еще не открыта
	if (position == 0) {
		// Бычий тренд, а бумага отстала
		if (deriv > 1 && k < -rate_openpos) {
			trade_action::buy(1, ::lots);
		}
		if (deriv < 1 && k > rate_openpos) {
			trade_action::sell(1, ::lots);
		}
	}
	// Если в лонге
	else if (position == 1 && k > -rate_closepos) {
		trade_action::sell(1, ::lots);
	}
	// Если в шорте
	else if (position == -1 && k < rate_closepos) {
		trade_action::buy(1, ::lots);
	}
}

В общем-то эта стратегия легко модифицируется под множество других аналогичных. Например, можно вместо закрытия прошлого дня использовать открытие текущего, для этого надо к getCandleByTime(getTimeObject(0, 0)) прибавить единицу, а вместо close использовать open. Либо отклонение можно считать не от самого изменения индекса, а от его скользящей средней — тогда изначально надо рассчитывать deriv по скользяней средней.

Следующий пример показывает каким образом мы можем рассчитывать свои собственные индексы и выводить их в переменную окражения:

extern weight = 1;
 
static last_val = 0;
 
function init()
{
	var x = getEnvVariable("myIndex");
	last_val = weight * close[countCandles() - 1];
	x += last_val;
	setEnvVariable("myIndex", x);
}
 
 
function onStopIndicator(var evnt)
{
	var x = getEnvVariable("myIndex");
	x -= last_val;
	setEnvVariable("myIndex", x);
}
 
function calc()
{
	var x = getEnvVariable("myIndex");
	x -= last_val;
	last_val = close * weight;
	x += last_val;
	setEnvVariable("myIndex", x);
}

Этот «индикатор» при каждом изменении цены бумаги, будет корректировать значение переменной окружения myIndex (изначально она долждна быть задана), что можно будет наблюдать в таблице окружения ATF, либо использовать в других индикаторах. Параметр weight задает вес данной бумаги в нашем «индексе».

В принципе диалог «Окружение ATF» можно использовать для отслеживания каких-то ключевых параметров. Туда можно выводить, например, статистические данные, по аналогии с тем, как мы выводили наш идект или отклонение. Можно так же использовать диалог «Окружение ATF» для задания каких-то глобальных параметров скриптов.

15. Тестирование торговых стратегий

Начиная со сборки 291, в Transaq доступно тестирование торговых систем (пока в урезанном виде, но какое-то представление о характеристиках торговой системы уже можно получить). Для начала тестирования необходимо войти в список скриптов ATF, выбрать скрипт для тестирования и нажать на «Test». В появившемся диалоге необходимо ввести начальные средства для тестирования, выбрать формат даты в файле данных и сам файл.

Пример начала файла с данными для тестирования стратегий:

<name>,<date>,<time>,<open>,<high>,<low>,<close>,<volume>,<o/i>
GAZP T+4,10/17/11,10:11,163.09,163.26,163.06,163.26,3022,264788
GAZP T+4,10/17/11,10:12,163.04,163.10,163.00,163.00,8011,262344
GAZP T+4,10/17/11,10:13,162.94,163.08,162.94,163.08,1538,260468
GAZP T+4,10/17/11,10:14,163.15,163.15,163.07,163.15,3922,259678
GAZP T+4,10/17/11,10:15,163.25,163.45,163.25,163.41,660,259318
GAZP T+4,10/17/11,10:16,163.38,163.38,163.24,163.24,720,259678
GAZP T+4,10/17/11,10:17,163.18,163.23,163.17,163.23,804,259142
GAZP T+4,10/17/11,10:18,163.20,163.20,163.20,163.20,529,258084
GAZP T+4,10/17/11,10:19,163.10,163.10,163.07,163.07,530,258802
GAZP T+4,10/17/11,10:20,163.06,163.06,163.06,163.06,240,258802
GAZP T+4,10/17/11,10:21,163.09,163.09,163.09,163.09,100,258602
GAZP T+4,10/17/11,10:25,162.57,162.70,162.57,162.70,2336,261660
GAZP T+4,10/17/11,10:26,162.54,162.63,162.54,162.63,224,261660
GAZP T+4,10/17/11,10:27,162.88,162.88,162.88,162.88,180,261660
GAZP T+4,10/17/11,10:29,163.12,163.18,163.12,163.18,624,262060
GAZP T+4,10/17/11,10:31,163.23,163.23,163.16,163.16,1194,262328
GAZP T+4,10/17/11,10:32,163.12,163.14,163.07,163.07,3739,269584
GAZP T+4,10/17/11,10:33,163.18,163.23,163.18,163.23,660,270904
GAZP T+4,10/17/11,10:34,163.40,163.40,163.25,163.25,2620,268904
GAZP T+4,10/17/11,10:35,163.32,163.38,163.31,163.36,6334,270744
GAZP T+4,10/17/11,10:36,163.38,163.42,163.38,163.38,1620,271202

В первой строке в заголовке перечисляются данные, содержащиеся в файле в том порядке, в какоим они далее записаны. Заголовок и определяет формат файла. Затем в каждой строчке файла содержатся непосредственно данные, в том порядке, в каком они были перечислены в заголовке. В качестве разделителя записей может выступать табуляция, пробел, запятая и точка с запятой. Разделитель в заголовке должен быть тот же, что и в остальной части файла. Поля, которые умеет воспринимать тестировщик ATF: TICKER, PER, DATE, TIME, OPEN, HIGH, LOW, CLOSE, VOL. Время должно быть в формате ччммсс, ччмм, чч:мм:cc или чч:мм. Формат представления даты необходимо выбрать в диалоге из раскрывающегося меню. Это стандартный формат данных, в котором различные онлайн-сервисы предоставляют исторические данные для анализа своим клиентам, в этом же формате Transaq делает экспорт в текстовые файлы из графиков и сохраняет данные для использования в оффлайне (их можно найти в директории cache2).

После закрузки свечей из файла (если файл большой, это может занять некоторое время), в диалоге будут заполнены поля «количество свечей» и «период» (он определяется автоматически). Далее после нажатия на кнопку «OK» начинается процесс тестирования торговой системы. Он может занять некоторое время в зависимости от сложности скрипта и объема данных.

Тестирование производится по упрощенной схеме — первая сотня свечей загружается из файла сразу. В дальнейшем на каждую свечу генерируется по четыре сделки на уровне open, high, low и close. Каждая сделка обрабатывается ATF ровно так же, как и при совершении сделок на реальном рынке. На данный момент тестировщик обрабатывает лишь базовые виды обычных и условных заявок. Стоп-заявки и заявки с дополнительными параметрами (usecredit, nosplit, unfilled, validbefore) не обрабатываются. Во время расчета индикатора в окне графика выводится процент рассчитанных данных. По завершению рассчета выводится график, с отмеченными на нем сделками, а так же отчет о различных статистических показателях торговой системы. Значения в таблице чаще всего имеют прозрачный смысл («Начальный депозит» или «Чистая прибыль»), поэтому приведем лишь некоторые комментарии.

Под общей прибыльностью и убытком подразумевается результат либо лишь прибыльных, либо лишь убыточных сделок соответственно. Чистая прибыль — это разница между общей прибылью и общим убытком, прибыльность — это их отношение.

Под результатом лонга или шорта в ATF подразумевается изменение денежных средств от момента открытия позиции, до полного ее закрытия. Например, если мы купили 1 акцию ОАО «XXX» по 100 рублей, затем докупили еще одну акцию по 105 рублей, затем продали акцию по 107 рублей и затем последнюю оставшуюся продали по 103 рубля, то вся эта серия будет учитываться как один прибыльный лонг прибыльная сделка, финансовым результатом которого будет доход в 5 рублей. Показатели шорты и лонги показывают количество соответствующих позиций за время тестирования в абсолютных и относительных показателях. По этим же данным рассчитываются показатели максимальных, минимальных и средних выигрышей и проигрышей по лонгам и шортам. Матожидание выигрыша показывает средневзвешенный доход (убыток) по всем позициям.

Непрерывный выигрыш или проигрыш — это суммарный доход или убыток от серии последовательных выигрышей или проигрышей. Аналогично считаются серии выигрышей или проигршей, но только если в первом случае существенно абсолютное значение выигрыша или проигрыша, то во втором существенным является лишь количество последовательно открытых прибыльных (убыточных) позиций, а не их финансовый результат.

Абсолютная просадка — это разница между начальным депозитом и наименьшей оценкой портфеля. Максимальная просадка считается так же, только вместо начального депозита берется локальный максимум в оценке портфеля.

1) Механическая торговая система.
2) Или бара. Зависит от выбранного Типа графика для данного окна графика. В этом руководстве будем считать, что Тип графика установлен в значение Японские свечи.
3) Деление на ноль в данном конкретном случае приведёт лишь к периодическим сообщениям об ошибке при обработке исторических данных, однако никак не повредит расчётам. В остальных случаях последствия могут быть самыми непредсказуемыми, поэтому деления на ноль следует всегда избегать.
atf/руководство.txt · Последние изменения: 2013/05/13 12:04 — heller