Vladimir Kozlov
0
All posts from Vladimir Kozlov
Vladimir Kozlov in Vladimir Kozlov,

Руководство "TRANSAQ ATF"

1. Основные принципы (массив line, функция calc)

Основа любого индикатора — это функция calc(). Данная функция собственно и содержит формулу для расчета индикатора. Минимальный пример допустимого индикатора:

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

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

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

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

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

Разберемся с тем, как TRANSAQ вычисляет такой индикатор. Для каждой свечки TRANSAQ выполняет функцию calc(), в которой задаются значения line[i], соответствующие различным линиями индикатора. Максимум индикатор может содержать 10 линий, от line[0] до line[9]. Обозначать количество линий не требуется — TRANSAQ сам определяет, сколько линий задействовал пользователь. Если какая-либо линия будет пропущена, то TRANSAQ заполнит ее нулями.

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

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

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

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

Ключевые слова high и low обозначают максимум и минимум текущей свечи.

Рассмотрим процесс расчета индикатора пошагово:

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

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

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

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

2. Сдвиги и управление границами

На практике индикаторов, использующих значения только текущей свечи, практически не существует — почти все индикаторы рассчитывается с использованием как минимум некоторой истории, а часто и с использованием предыдущих своих значений и значением других своих линий. Для ссылки на значение за границами текущей свечки к используемому параметру свечи (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];
На практике необходимость ссылаться на линии, которые отображаются со сдвигом, появляется крайне редко. Это не самый простой прием, который потенциально ведет к путанице и ошибкам, и чаще всего он может быть заменен более простой альтернативой, как в примере выше.

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

Под контролем выполнения понимаются конструкции, позволяющие изменить последовательный ход выполнения программы в зависимости от определенных условий. Вспомним пример индикатора 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 — это просто значение по умолчанию, которое можно переопределить из диалога индикатора, не прикасаясь к исходному коду.

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

Примерно половина всех индикаторов не обходится без использования промежуточных скользящих средних и среднеквадратичного отклонения. Простейший пример индикатора, который использует и то и другое — 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.

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

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

В 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() проверяет доступное количество свечек на графике, и если их недостаточно, останавливает расчет индикатора, а пользователю показывается сообщение о недостатке истории. Большой необходимости в этой функции нет, так как даже без нее никаких ошибок не произойдет, однако ее применение в уже готовом отлаженном индикаторе позволяет информировать пользователя о недостатке истории. В противном случае индикатор просто не рассчитается, что без объявления причин может выглядеть весьма странно.

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

Пользователь может определять собственные функции в 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. Это невозможно написать чисто синтаксически. В действительности с этим нет большой проблемы, так как появление таких сложных рекурсий почти всегда свидетельствует об ошибке и о том, что надо пересмотреть реализацию алгоритма.

Важно отдельно сказать о том, что внутри определенных пользователем функций не желательно использование массива line, а также прямых ссылок на свечи (close, high, low и т. д.). На данный момент line реализован как глобальная переменная, доступная из любой части программы, то есть внутри любой функции можно заполнять массив line, при этом смысл операции будет зависеть от того, какая функция выполняется в данный момент — calc или init. Это весьма опасный и неочевидный подход, и в дальнейшем очень вероятно, что использование line и ссылки на свечи в функциях, отличных от calc и init, будет запрещен.

7. Сигналы

Сигнал — это автоматическое уведомление пользователя о том, что произошло некоторое событие. В 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)
Проиграть указанный файл (при указании пути до файла необходимо заменять символ «\» на «\\» — в следующем разделе это объясняется подробнее).

8. Строки

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

...
	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-число по умолчанию

9. Взаимодействие 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 процентов (эта величина задается пользователем), срабатывает сигнал, и текущее значение накопленного изменения цены в дальнейшем будет сравниваться уже с большей величиной.

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

Механическая торговля ничем принципиально не отличается от подачи сигналов. Существует четыре функции для выставления заявки на рынок: 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.

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

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

extern 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 onNewCandle()
{
	signal::alert()
}

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;

function init()
{
	buffer = new_object("linebuffer");
	setBound
}

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 ничем в этом смысле не ограничены.

При использовании объектов 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). Список полей, которые возможно заполнить для заявки и их значения, смотрите в разделах «Объекты» и «Константы».

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

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

Для многих стратегий оказывается оказывается полезным созранять данные между сессиями или запусками 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

%R

extern period = 27;

function init() {
  setInitCandles(period - 1);
  setBounds(0, period - 1, 0);
}

function calc() {
  var p = 1 - period;
  var hi = high[p, 0];
  line[0] = -100 * (hi - close) / (hi - low[p, 0]);
}

A/D

function init()
{
  setInitCandles(1);
  line[0] = (2 * close - high - low) * volume / (high - low);
}

function calc()
{
  if (high == low) {
    line[0] = line[0][-1];
  }
  else {
	line[0] = line[0][-1] +
        (2 * close - high - low) * volume / (high - low);
  }

}

Alligator

#samewindow
#line 0 solid blue
#line 1 solid red
#line 2 solid green
extern period1 = 13;
extern period2 = 8;
extern period3 = 5;
extern lag1 = 8;
extern lag2 = 5;
extern lag3 = 2;

function init()
{
	setBounds(0, lag1, lag1);
	setBounds(1, lag2, lag2);
	setBounds(2, lag3, lag3);
}

function calc()
{
	line[0][lag1] = MovAvg(ind_ema, period1, pt_med);
	line[1][lag2] = MovAvg(ind_ema, period2, pt_med);
	line[2][lag3] = MovAvg(ind_ema, period3, pt_med);
}

AO

#line 0 colored_hist

extern period1 = 5;
extern period2 = 34;

function init()
{
	var max = period1;
	if (period2 > max) {max = period2;}
	setBounds(0, max - 1, 0);
}

function calc()
{
	line[0] = MovAvg(ind_sma, period1, pt_close) -
		MovAvg(ind_sma, period2, pt_close);
}

ATR

extern period = 27;

function init()
{
	setInitCandles(2);
	setBounds(0, 1, 0);
	var tr = high[1] - low[1];
	if (tr < high[1] - close) {tr = high[1] - close;}
	if (tr < close - low[1]) {tr = close - low[1];}
	line[0][1] = tr;
}

function calc()
{
	var tr = high - low;
	if (tr < high - close[-1]) {tr = high - close[-1];}
	if (tr < close[-1] - low) {tr = close[-1] - low;}
	var alpha = 2 / (period + 1);
	line[0] = alpha * tr + (1 - alpha) * line[0][-1];
}

Bollinger Bands

#samewindow
#line 0 solid #00C000
#line 1 solid #006000
#line 2 solid #006000

extern ma_period = 20;
extern sd_period = 20;
extern k = 2;

function init()
{
	var m = ma_period;
	if (m < sd_period) {m = sd_period;}
	setInitCandles(m);
	setBounds(0, m, 0);
	setBounds(1, m, 0);
	setBounds(2, m, 0);
}

function calc()
{
	var sd = StdDev(stddev_abs, sd_period, pt_close);
	var v = MovAvg(ind_sma, ma_period, pt_close);
	line[0] = v;
	line[1] = v + k * sd;
	line[2] = v - k * sd;
}

CCI

extern period = 20;
extern var_period = 20;
extern k = 0.015;

function init()
{
	var m = period;
	if (m < var_period) {m = var_period;}
	setInitCandles(m);
	setBounds(0, m, 0);
}

function calc()
{
	var m = 0;
	var md = 0;
	var i = 0;
	while (i < var_period) {
		m += typical[-i] / var_period;
		i += 1;
	}

	i = 0;
	while (i < var_period) {
		md += abs(typical[-i] - m) / var_period;
		i += 1;
	}


	line[0] = (typical - MovAvg(ind_sma, period, pt_typical)) / (k * md);
}

CMO

extern period = 27;

function init()
{
	setInitCandles(period);
	setBounds(0, period, 0);
}

function calc()
{
	var sumUp = 0;
	var sumDown = 0;

	var i = 0;
	while (i < period) {
		var d = close[-i] - close[-i - 1];
		if (d > 0) {sumUp += d / period;}
		else {sumDown += -d / period;}
		i += 1;
	}

	line[0] = 100 * (sumUp - sumDown) / (sumUp + sumDown);
}

DeMarker

#line 0 nodraw
#line 1 nodraw

extern period = 17;

function init()
{
	setInitCandles(1);
	setBounds(2, period, 0);
}

function calc()
{
	if (high > high[-1]) {line[0] = high - high[-1];}
	if (low < low[-1]) {line[1] = low[-1] - low;}
	var sma_demax = MovAvg(ind_sma, period, line[0]);
	var sma_demin = MovAvg(ind_sma, period, line[1]);
	var d = sma_demin + sma_demax;
	if (d > 0) {
		line[2] = sma_demax / (sma_demin + sma_demax);
	}
}

Chaikin Volatility

extern period = 10;

var emaold;
var emacur;

function init()
{
	setInitCandles(period);
	emaold = high - low;
	emacur = high[period] - low[period];
	line[0] = 100 * (emacur - emaold) / emaold;
	setBounds(0, period, 0);
}

function calc()
{
	var alpha = 2 / (period + 1);
	emaold = alpha * (high[-period] - low[-period]) +
		(1 - alpha) * emaold;
	emacur = alpha * (high - low) + (1 - emacur) * emacur;
	line[0] = 100 * (emacur - emaold) / emaold;
}

EFI

extern period = 13;

function init()
{
	setInitCandles(2);
	setBounds(0, 1, 0);
	line[0][1] = (close[1] - close) * volume[1];
}

function calc()
{
	var alpha = 2 / (period + 1);
	line[0] = alpha * volume * (close - close[-1]) +
		(1 - alpha) * line[0][-1];
}

Elder-rays

extern period = 10;
 
function calc()
{
	var ema = MovAvg(ind_ema, period, pt_close);
	line[0] = high - ema;
	line[1] = low - ema;
}

Ichimoku Kinko Hyo

#samewindow

extern n1 = 9;
extern n2 = 26;
extern n3 = 52;

function init()
{
	setInitCandles(n3);
	setBounds(0, n3, 0);
	setBounds(1, n3, 0);
	setBounds(2, n3, -n2);
	setBounds(3, n3, -n2);
	setBounds(4, n3 * 2, n2);
}

function calc()
{
	line[0] = (high[-n1, 0] + low[-n1, 0]) / 2;
	line[1] = (high[-n2, 0] + low[-n2, 0]) / 2;
	line[2][-n2] = (line[0] + line[1]) / 2;
	line[3][-n2] = (high[-n3, 0] + low[-n3, 0]) / 2;
	line[4][n2] = close;
}

Impulse system (A. Elder)

#line 0 nodraw
#line 1 nodraw
#line 2 hist

extern ma_period = 13;
extern macd1 = 12;
extern macd2 = 26;
extern macd_signal = 9;

function init()
{
   setInitCandles(1);
   setBounds(2, 1, 0);
}

function calc()
{
    line[0] = MovAvg(ind_ema, ma_period, pt_close);
   line[1] = IndRef("macdhistogram", macd1, macd2, macd_signal,
      ind_ema, ind_ema, ind_ema, pt_close)[0];

   if (line[0] > line[0][-1] && line[1] > line[1][-1]) {
      line[2] = 1;
   }
   if (line[0] < line[0][-1] && line[1] < line[1][-1]) {
      line[2] = -1;
   }
}

MA Envelope

#samewindow
#line 0 solid #00c000
#line 1 solid #00c000

extern period = 14;
extern k = 2;


function calc()
{
	var ma = MovAvg(ind_ema, period, pt_close);
	line[0] = ma * (1 + k/1000);
	line[1] = ma * (1 - k/1000);
}

ParabolicSAR

#samewindow
#line 0 dot maroon

extern step = 0.02;
extern init_step = 0.02;
extern max_step = 0.20;

var ac;
var trend;
var ep;

function init()
{
	setInitCandles(1);
	if (low < low[1]) {
		trend = 1;
		ep = high[0, 1];
		line[0] = low;
	}
	else {
		trend = -1;
		ep = low[0, 1];
		line[0] = high;
	}
	ac = init_step;
}

function calc()
{
	if (trend == 1) {
		if (high > ep) {
			ep = high;
			ac += step;
			if (ac > max_step) {ac = max_step;}
		}
		line[0] = line[0][-1] + ac * (ep - line[0][-1]);
		if (low < line[0]) {
			trend = -1;
			line[0] = ep;
			ep = low;
			ac = init_step;
		}
	}
	else {
		if (low < ep) {
			ep = low;
			ac += step;
			if (ac > max_step) {ac = max_step;}
		}
		line[0] = line[0][-1] + ac * (ep - line[0][-1]);
		if (high > line[0]) {
			trend = 1;
			line[0] = ep;
			ep = high;
			ac = init_step;
		}
	}
}

RAVI

extern period1 = 7;
extern period2 = 65;

function init()
{
   setInitCandles(period2);
   setBounds(0, period2, 0);
}

function calc()
{
   var m1 = MovAvg(ind_sma, period1, pt_close);
   var m2 = MovAvg(ind_sma, period2, pt_close);
   line[0] = abs(100 * (m1 - m2) / m2);
}

RSI (EMA based)

extern period = 14;
 
var up;
var down;
 
function init()
{
	up = 0;
	down = 0;
	setInitCandles(period - 1);
	setBounds(0, period - 1, 0);
	var i = 1;
	while (i <= period) {
		var delta = close[i] - close[i - 1];
		if (delta > 0) {up += delta;}
		else {down -= delta;}
		i += 1;
	}
	up /= period - 1;
	down /= period - 1;
 
	line[0][period] = 100 - 100 / (1 + up / down);
}
 
function calc()
{
	var delta = close - close[-1];
	var u = 0;
	var d = 0;
	if (delta > 0) {u = delta;}
	else {d = -delta;}
	up = (up*(period - 1) + u) / period;
	down = (down*(period - 1) + d) / period;
	line[0] = 100 - 100 / (1 + up / down);
}

ZigZag

#samewindow
#line 0 solid red
extern rate = 1;
var trend = 1;
var last;
var last_n;
var last_extr;
var last_extr_n = 0;
var r;


function init() {
	last = close;
	line[0] = close;
	last_extr = close;
	last_extr_n = 0;
	last_n = 0;
	r = rate / 100;
}


function getCandleLag(var n, var curr)
{
	return -(curr - n);
}


function approxLinear(var x1, var y1, var x2, var y2) 
{
	var n = x2 - x1;
	if (!n) {
		line[0][getCandleLag(x1, noCandle())] = y1;
		return;
	}
	var k = (y2 - y1) / n;
	var i = 0;
	while (i <= n) {
		line[0][getCandleLag(x1 + i, noCandle())] = y1 + k*i;
		i += 1;
	}
}


function findMax(var from, var to)
{
	var m = high[getCandleLag(from, noCandle())];
	var n = from;
	var i = from + 1;
	while (i <= to) {
		var x = high[getCandleLag(from, noCandle())];
		if (x > m) {
			m = x;
			n = i;
		}
		i += 1;
	}
	return n;
}


function findMin(var from, var to)
{
	var m = low[getCandleLag(from, noCandle())];
	var n = from;
	var i = from + 1;
	while (i <= to) {
		var x = low[getCandleLag(from, noCandle())];
		if (x < m) {
			m = x;
			n = i;
		}
		i += 1;
	}
	return n;
}



function calc()
{
	if (trend == 1) {
		if (close > last_extr) {
			last_extr = high;
			last_extr_n = noCandle();
			approxLinear(last_n, last, noCandle(), high);
		}
		else {
			approxLinear(last_extr_n, last_extr, noCandle(), low);
		}
	}
	if (trend == -1) {
		if (close < last_extr) {
			last_extr = low;
			last_extr_n = noCandle();
			approxLinear(last_n, last, noCandle(), low);
		}
		else {
			approxLinear(last_extr_n, last_extr, noCandle(), high);
		}
	}

	if (abs(close - last_extr) / last_extr > r) {
		var old_n = last_n;
		var old = last;
		last_n = last_extr_n;
		last = last_extr;
		if (trend > 0) {
			trend = -1;
			last_extr_n = findMin(last_n, noCandle());
			last_extr = low[getCandleLag(last_extr_n, noCandle())];
		}
		else {
			trend = 1;
			last_extr_n = findMax(last_n, noCandle());
			last_extr = high[getCandleLag(last_extr_n, noCandle())];
		}
		approxLinear(old_n, old, last_n, last);
		approxLinear(last_n, last, noCandle(), close);
	}
}

Pivot points

#samewindow
#line 0 solid blue
#line 1 dashed green
#line 2 dashed red
#line 3 dot green
#line 4 dot red

var day; // current day
var nday; // number of day in chart
var ph; // high of yesterday
var pl; // low of yesterday
var pc; // close of yesterday
var cl; // today's low
var ch; // today's high

function init()
{
	nday = 1;
	day = getDay(getCandleTime());
}


function drawLines()
{
	line[0] = (ph + pl + pc) / 3; // Pivot point
	line[1] = 2*line[0] - ph; // Support 1
	line[2] = 2*line[0] - pl; // Resistance 1
	line[3] = line[0] - (line[2] - line[1]); // Resistance 2
	line[4] = line[0] + (line[2] - line[1]); // Support 2
}


function calc()
{
	if (nday == 1) {
		if (getDay(getCandleTime()) != day) {
			nday = 2;
			day = getDay(getCandleTime());
			ph = high;
			pl = low;
		}
	}
	else if (nday == 2)	{
		if (getDay(getCandleTime()) != day) {
			nday = 3;
			day = getDay(getCandleTime());
			cl = low;
			ch = high;
			pc = close[-1];
			drawLines();
			setBounds(0, noCandle(), 0);
			setBounds(1, noCandle(), 0);
			setBounds(2, noCandle(), 0);
			setBounds(3, noCandle(), 0);
			setBounds(4, noCandle(), 0);
		}
		else {
			if (high > ph) {ph = high;}
			if (low < pl) {pl = low;}
		}
	}
	else {
		if (getDay(getCandleTime()) != day) {
			nday += 1;
			day = getDay(getCandleTime());
			ph = ch;
			pl = cl;
			pc = close[-1];
			cl = low;
			ch = high;
		}
		else {
			if (high > ch) {ch = high;}
			if (low < cl) {cl = low;}
		}
		drawLines();
	}
}

Stochastic Momentum Index

#line 0 nodraw
#line 1 nodraw
#line 2 nodraw
#line 3 nodraw
#line 4 nodraw
#line 5 nodraw
#line 7 solid red

extern lp = 13;
extern sm = 25;
extern dsmp = 2;
extern sig = 3;


function init()
{
	if (countCandles() < lp) {
		lackHistory();
	}
	setInitCandles(lp);
	setBounds(0, lp, 0);
}


function calc()
{
	line[0] = close - .5 * high[-lp,0] + low[-lp,0];
	line[1] = MovAvg(ind_ema, sm, line[0]);
	line[2] = MovAvg(ind_ema, dsmp, line[1]);

	line[3] = high[-lp, 0] - low[-lp, 0];
	line[4] = MovAvg(ind_ema, sm, line[3]);
	line[5] = 0.5*MovAvg(ind_ema, dsmp, line[4]);

	line[6] = 100 * line[2] / line[5];
	line[7] = MovAvg(ind_ema, sig, line[6]);
}

Ultimate Oscillator

// Данный индикатор может быть существенно оптимизирован
// с точки зрения скорости расчета. Улучшенный пример
// будет приведен позже
#line 0 nodraw
#line 1 nodraw
extern period = 7;

function init()
{
	if (countCandles() < period * 4 + 1) {
		lackHistory();
	}
	setInitCandles(1);
	setBounds(0, period*4 + 1, 0);
}


function sumLine1(var n)
{
	var i = 0;
	var s = 0;
	while (i < n) {
		s += line[0][-i];
		i += 1;
	}
	return s;
}

function sumLine2(var n)
{
	var i = 0;
	var s = 0;
	while (i < n) {
		s += line[1][-i];
		i += 1;
	}
	return s;
}


function avg(var n)
{
	return sumLine1(n) / sumLine2(n);
}


function calc()
{
	if (low < close[-1]) {
		line[0] = close - low;
	}
	else {
		line[0] = close - close[-1];
	}

	var h;
	var l;
	if (high > close[-1]) {h = high;}
	else {h = close[-1];}
	if (low < close[-1]) {l = low;}
	else {l = close[-1];}
	line[1] = h - l;

	if (noCandle() > period*4 + 1) {
		line[2] = 100 * (4*avg(period) + 2*avg(period*2) + avg(period*4)) / 7;
	}
}

Сигнал: пересечение ценой скользящей средней

#samewindow
#line 0 solid red

extern period = 9;
var trend = 0;

function init()
{
	setInitCandles(1);
}


function calc()
{
	line[0] = MovAvg(ind_ema, period, pt_close);
	
	if (trend == 1 and close < line[0]) {
		signal::alert("Продажа: " + getSecName() +
					" пересекла сверху вниз скользящую среднюю.");
	}
	
	if (trend == -1 and close > line[0]) {
		signal::alert("Покупка: " + getSecName() +
					" пересекла снизу вверх скользящую вреднюю.");
	}
	
	if (close > line[0]) {trend = 1;}
	else if (close < line[0]) {trend = -1;}
}

Сигнал: пересечение двух скользящих средних

#samewindow
#line 0 solid red

extern fast = 9;
extern slow = 14;
var trend = 0;

function init()
{
	setInitCandles(1);
}


function calc()
{
	line[0] = MovAvg(ind_ema, fast, pt_close);
	line[1] = MovAvg(ind_ema, slow, pt_close);
	
	if (trend == 1 and line[0] < line[1]) {
		signal::alert("Продажа: по " + getSecName() +
						" быстрая MA пересекла медленную сверху вниз.");
	}
	
	if (trend == -1 and line[0] > line[1]) {
		signal::alert("Покупка: по " + getSecName() +
						" медленная MA пересекла быструю снизу вверх.");
	}
	
	if (line[1] > line[0]) {trend = 1;}
	else if (line[1] < line[0]) {trend = -1;}
}

Сохранение данных в файл

// Данный код не несет полезной функии,
// а лишь демонстрирует работу с файлами.
// Подробности смотрите в руководстве.


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;
}

Сохранение информации о сделках в файл

// Данный код не несет полезной функии,
// а лишь демонстрирует работу с файлами.
// Подробности смотрите в руководстве.

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);
}


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

©2009—2010 ЗАО «Скрин Маркет Системз»