Доброго времени суток. В этой статье расскажу о создании системы в которой генетические алгоритмы пишут роботов. В теории эти роботы могли бы торговать на бирже.

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

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

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

С чего бы начать?


Я начал с изучения платформы для создания роботов MetaTrader5. Язык MQL5 позиционируется как схожий с С++, с незначительными отличиями в синтаксисе. Если говорить простыми словами, в платформе имеются функции для доступа к данным рынка и функции для выполнения торговых операций. После изучения и проверки нескольких десятков простых роботов, началась работа над их выделением общей элементарной базы, на которой и строятся эти алгоритмы.

Для удобства работы с логикой внутри генетического алгоритма мне пришлось создать свой мета-язык над MQL, назовем его SadLobster. Без этого обобщения было бы ужасно сложно заставить машину писать код по правилам языка программирования созданного для человека. Весь проект был обозначен как прототип, чтобы было проще принять множество компромиссов и упрощений. Иначе эта фаза разработки никогда бы не закончилась.

Как работает один робот


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

Код робота
// NoPos() или YesPos() вызываются каждый бар
// если нет открытой позиции
void NoPos(bool invert){
        // попытка выставить стоп ордер по цене priceA__6 если boolA__3 == true
        PUT_ORDER(boolA__3, priceA__6, STOP_ONLY);
}
// если есть открытая позиция
void YesPos(bool invert){
        // попытка выставить stop loss по цене priceA__10
        PUT_SL_ON_PRICE(priceA__10);
}

//эта функция говорит выставлять ли ордер
DEF_BOOL boolA__3(bool invert) {
	DEF_OFFSET var_2 = __value(1);
	DEF_PRICE var_4 = _HIGH(var_2, invert);
	DEF_PIPS_DOUBLE var_1 = MA_RANGE(8, dsD1, 1);
	DEF_PRICE var_0 = MA_HI_I(7, ds, 1, !invert);
	DEF_HPRICE_LEVEL var_3 = MAKE_HPRICE_LEVEL(var_0, var_1);
	DEF_BOOL var_5 = IS_INSIDE(var_3, var_4);
	return var_5;
}

//эта функции сообщает по какой цене выставлять ордер
DEF_PRICE priceA__10(bool invert) {
	DEF_PRICE var_2 = _HIGH_D1(1, invert);
	DEF_PRICE var_1 = _LOW_D1(1, invert);
	DEF_WAVE_INDEX var_0 = CALL_FUNC(waveState_38);
	DEF_BOOL var_3 = IS_WAVE(var_0, 1);
	DEF_PRICE var_4 = IF_ELSE(var_3, var_1, var_2);
	return var_4;
}

Функции boolA__3 и priceA__10 обрабатывают информацию, получаемую с графиков котировок.
Функция boolA__3 запускается чтобы проверить есть ли сигнал для выставления ордера. Первый раз мы проверяем есть ли сигнал на покупку. Второй раз запускаем еще со значением инверт=1 и проверяем есть ли сигнал на продажу.

Функция priceA__10 определяет по какой цене должен быть выставлен ордер.

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

MQL tester vs C++ tester

  • Этот тестер на два порядка быстрее MQL и имеет необходимое API чтобы им мог управлять генетический алгоритм.
  • MQL же предоставляет отличные возможности для отладки и проверки правильной работы роботов.

В применении к торговым роботам есть такой термин грааль — это робот, который зарабатывает много и стабильно даже вне обучающей выборки. В ходе разработки я встречал их очертания несколько раз. И каждый из них был результатом уязвимости в С++ тестере. По мере эволюции, роботы находили уязвимости во фреймворке тестирования — проводили невозможные операции или находили способ заглянуть в будущие данные и много других хитростей. (Мне кажется потенциал генетического программирования в тестировании сильно недооценен.) Здесь на помощь приходил MQL. Запуская робота там, он терял волшебные свойства грааля, потому как там большинство уязвимостей уже прикрыты.

Язык состоит из списка функций которые можно использовать. Простейшие — AND, OR, CREATE_LINE, IS_INSIDE,…

И функции доступа к данным котировок и технических индикаторов — HIGH, LOW, FRACTAL, MA, MACD_SIGNAL. Эти функции будут перечислены в списке 1.

Симуляция торговли на истории


Робот запускается на периоде истории, например с 2014 по 2016 год. Происходит моделирование торговли. Все его сделки записываются и по ним формируется отчет. Мой отчет выглядит примерно так:



или так

1.82  14.66   64.1%   1.02   -383[+0.99]    451 (30.8%)    +6613 :     179F  <736c> 

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

По отчету видно хорош робот или нет. Про тестер стратегий и его реализацию постараюсь рассказать в другой раз.

Фитнес функция


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

Первое решение — чем больше робот заработал, тем он лучше. Но тут возникает вопрос рисков. Такой робот совершенно нежизнеспособен. Меньше риск — меньше прибыль, больше риск больше прибыль.

У торговых роботов есть несколько различных характеристик. Самые простые из них — профит фактор(PF) и математическое ожидание прибыли на одну сделку(EP), максимальная просадка по средствам, LR correlation, Коэффициент Шарпа.

Вот так выглядит отчет MetaTrader о работе одного из созданных роботов:



У каждого из параметров есть свой коэффициент важности. Пропорционально этим числам вычисляется фитнесс функция для каждого робота. После чего происходят хорошо известные процессы скрещивания и мутации. И еще дополнительно установлен порог минимального количества сделок. От 0.2 до 2-х сделок в день, минимум.

self.KOEF = [2, 4, 1, 1, 1, 1, 0, 0, 2]
self.KEYS = ['PF', 'EP', 'win_persent', 'p_wiin_div_loss', 'max_dd', 'deals', 'profit', 'pfMonth', 'LR']

Динамика и результаты запуска Генетического Алгоритма


Графическое представление эволюции или график обучения



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

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

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

О сложности


Алгоритм робота для простоты не имеет внутренней памяти или состояний. Эта же особенность помогает кешировать результаты вычислений на каждом баре. Что сильно ускоряет вычисления. Стараясь использовать только функции со сложностью О(1) или O(n) в логике, я сильно ограничил функционал. Но этого требовали вычислительные ресурсы.

Генерация случайного дерева


Как получить функцию в том виде в котором она представлена в первом листинге?

  1. Надо создать список возможных функций и описать их
  2. Собрать случайное дерево-выражение которое и есть логика
  3. Преобразовать в код

Вот часть интерфейсных функций которые используются в логике роботов. Каждое имя функции это некий макрос, доступный как с MQL так и с тестового фреймворка С++. Реализации отличаются, в силу различий в языках. Назовем его список 1.

Краткий список функций. Список 1.
#EXAMPLE
{'name':'MORE_I', 'input':['DEF_PRICE','DEF_PRICE','invert'], 'result':'DEF_BOOL', 'price':4}
{'name':'_CLOSE', 'input':['DEF_OFFSET'], 'result':'DEF_PRICE', 'price':1}
{'name':'_HIGH', 'input':['DEF_OFFSET','invert'], 'result':'DEF_PRICE', 'price':1}
{'name':'__value', 'input':['1'], 'result':'DEF_OFFSET', 'price':1}
{'name':'_CLOSE_D1', 'input':['1'], 'result':'DEF_PRICE', 'price':1}

#OTHER
#ALGORITHMS
{'name':'CALL_FUNC_v1',                  'input':['FUNC_period'], 'result':'DEF_PERIOD', 'flags':['singleton']}
{'name':'CALL_FUNC_v2',                  'input':['FUNC_easyPrice'], 'result':'DEF_PRICE'}
{'name':'CALL_FUNC_v3',                  'input':['FUNC_easyPips'], 'result':'DEF_PIPS_DOUBLE' }

#wave
{'name':'CALL_FUNC_v4',                  'input':['FUNC_waveState'], 'result':'DEF_WAVE_INDEX', 'flags':['singleton'] }
{'name':'IS_WAVE',                       'input':['DEF_WAVE_INDEX','wave_count'], 'result':'DEF_BOOL' }

#DEF_PERIOD
{'name':'makePeriodSinceLastDay',           'input':['ds'], 'result':'DEF_PERIOD'}
{'name':'MAKE_PERIOD_v1',         'input':['6','60'], 'result':'DEF_PERIOD'}
{'name':'MAKE_PERIOD_v2',          'input':['DEF_OFFSET','DEF_OFFSET'], 'result':'DEF_PERIOD'}

#DEF_POINTS
{'name':'determinatePeriodsAboutClose',   'input':['ds','specArray1'], 'result':'DEF_POINTS'}
{'name':'DOWN_FRACTALS_ON_PERIOD',        'input':['ds','DEF_PERIOD','invert'], 'result':'DEF_POINTS'}
{'name':'GetZZPoints',                    'input':['zzPointsCount','ds','zzIndex'], 'result':'DEF_POINTS', 'flags':['singleton']}

#DEF_POINT
{'name':'MAX_PRICE_POINT',            'input':['ds','DEF_PERIOD','invert'], 'result':'DEF_POINT'}
{'name':'GetPoint_v1',                'input':['DEF_POINTS','pointIndex'], 'result':'DEF_POINT'}
{'name':'PROP_LINE_END',              'input':['DEF_LINE'], 'result':'DEF_POINT'}
{'name':'PROP_LINE_START',            'input':['DEF_LINE'], 'result':'DEF_POINT'}
{'name':'GetPoint',                   'input':['DEF_POINTS','pointIndexInZZ'], 'result':'DEF_POINT'}
{'name':'IF_ELSE_PO',                 'input':['DEF_BOOL','DEF_POINT','DEF_POINT'], 'result':'DEF_POINT'}
{'name':'MAXPOINT_I',                 'input':['DEF_POINTS','invert'], 'result':'DEF_POINT'}
{'name':'PROP_CENTER',                'input':['DEF_LINE'], 'result':'DEF_POINT'}

#DEF_PRICE
{'name':'PROP_PRICE',                 'input':['DEF_POINT'], 'result':'DEF_PRICE'} 
{'name':'PROP_PRICE_BY_OFFSET',       'input':['DEF_LINE','DEF_OFFSET'], 'result':'DEF_PRICE'} 
{'name':'_CLOSE',                     'input':['DEF_OFFSET'], 'result':'DEF_PRICE'} 
{'name':'_HIGH',                      'input':['DEF_OFFSET', 'invert'], 'result':'DEF_PRICE'} 
{'name':'_LOW',                       'input':['DEF_OFFSET', 'invert'], 'result':'DEF_PRICE'} 
{'name':'_OPEN',                      'input':['DEF_OFFSET'], 'result':'DEF_PRICE'} 
{'name':'GET_MEDIAN_CLOSE_PRICE',     'input':['DEF_PERIOD','ds'], 'result':'DEF_PRICE'} 
{'name':'IF_ELSE_v2',                 'input':['DEF_BOOL','DEF_PRICE','DEF_PRICE'], 'result':'DEF_PRICE'}
{'name':'CENTER_PRICE_BETWEEN_LINES', 'input':['DEF_LINE','DEF_LINE','DEF_OFFSET'], 'result':'DEF_PRICE'} 
{'name':'_CLOSE_D1',                  'input':['1'], 'result':'DEF_PRICE'} 
{'name':'_HIGH_D1',                   'input':['1','invert'], 'result':'DEF_PRICE'} 
{'name':'_LOW_D1',                    'input':['1','invert'], 'result':'DEF_PRICE'} 
{'name':'_OPEN_D1',                   'input':['1'], 'result':'DEF_PRICE'} 
{'name':'PRICE_MAX_I',                'input':['DEF_PRICE','DEF_PRICE','invert'], 'result':'DEF_PRICE'} 
{'name':'MATH_AVR_v2',                'input':['DEF_PRICE','DEF_PRICE'], 'result':'DEF_PRICE'} 
{'name':'ADD_PRICE_PIPS_v1',          'input':['DEF_PRICE','DEF_PIPS_DOUBLE','invert'], 'result':'DEF_PRICE'} 
{'name':'SYNC_MA',                   'input':['1','BARS_COUNT','ds'], 'result':'DEF_PRICE'} 
{'name':'MA_CLOSE_v1',                'input':['ma_bars_count','ds','DEF_OFFSET'], 'result':'DEF_PRICE'} 
{'name':'MA_HI_I_v1',                 'input':['ma_range_size','ds','1','invert'], 'result':'DEF_PRICE'} 
{'name':'STD_DEV_8',                  'input':['DEF_OFFSET'], 'result':'DEF_PIPS_DOUBLE'} 
{'name':'STD_DEV_20',                 'input':['DEF_OFFSET'], 'result':'DEF_PIPS_DOUBLE'} 

#DEF_SLOPE
{'name':'PROP_SLOPE',                 'input':['DEF_LINE'], 'result':'DEF_SLOPE'}

#DEF_LINE
{'name':'PROP_MIRROR_LINE',       'input':['DEF_LINE'], 'result':'DEF_LINE'}
{'name':'MAKE_SUPPORT',           'input':['DEF_POINTS','DEF_PERIOD','4','invert'], 'result':'DEF_LINE', 'check':'CHECK_LINE_OR_FALSE'}
{'name':'NewLine',                'input':['DEF_POINT','DEF_POINT'], 'result':'DEF_LINE', 'check':'CHECK_LINE_OR_FALSE'} 
{'name':'IF_ELSE_LL',             'input':['DEF_BOOL','DEF_LINE','DEF_LINE'], 'result':'DEF_LINE'}
{'name':'RegressionOnPointsV1',   'input':['DEF_POINTS'], 'result':'DEF_LINE'}

#DEF_OFFSET
{'name':'MAX_CANDLE',             'input':['ds','DEF_PERIOD'], 'result':'DEF_OFFSET'} 

#DEF_BOOL
{'name':'MORE_I',                 'input':['DEF_PRICE','DEF_PRICE','invert'], 'result':'DEF_BOOL'} 
{'name':'IS_INSIDE',              'input':['DEF_HPRICE_LEVEL','DEF_PRICE'], 'result':'DEF_BOOL',} 
{'name':'DIFF',               'input':['DEF_PRICE','DEF_PRICE','DEF_AWS'], 'result':'DEF_BOOL'} 
{'name':'DIFF_MORE',          'input':['DEF_PRICE','DEF_PRICE','DEF_AWS'], 'result':'DEF_BOOL'} 
{'name':'HAS_CROSS_FUTURE',   'input':['DEF_LINE','DEF_LINE','const_10'], 'result':'DEF_BOOL'} 
{'name':'IF_ELSE_v1',         'input':['DEF_BOOL','DEF_BOOL','DEF_BOOL'], 'result':'DEF_BOOL'} 
{'name':'MORE_v4',            'input':['DEF_PIPS_DOUBLE','DEF_PIPS_DOUBLE'], 'result':'DEF_BOOL'} 
{'name':'MORE_MULT',          'input':['DEF_PIPS_DOUBLE','DEF_PIPS_DOUBLE','float_fibo_mult'], 'result':'DEF_BOOL'} 
{'name':'AND2',               'input':['DEF_BOOL','DEF_BOOL'], 'result':'DEF_BOOL'} 
{'name':'AND3',               'input':['DEF_BOOL','DEF_BOOL','DEF_BOOL'], 'result':'DEF_BOOL'} 
{'name':'OR2',                'input':['DEF_BOOL','DEF_BOOL'], 'result':'DEF_BOOL'} 
{'name':'OR3',                'input':['DEF_BOOL','DEF_BOOL','DEF_BOOL'], 'result':'DEF_BOOL'} 
{'name':'NOT',                'input':['DEF_BOOL'], 'result':'DEF_BOOL'} 
{'name':'EQ_BOOL',            'input':['DEF_BOOL','DEF_BOOL'], 'result':'DEF_BOOL'} 
{'name':'PROP_IS_UP_I',       'input':['DEF_LINE','invert'], 'result':'DEF_BOOL'} 
{'name':'MORE_I_v3',          'input':['DEF_SLOPE','DEF_SLOPE','invert'], 'result':'DEF_BOOL'} 
{'name':'MORE_ABS',           'input':['DEF_SLOPE','DEF_SLOPE'], 'result':'DEF_BOOL'} 
{'name':'DIFF_MULT',          'input':['DEF_PIPS_DOUBLE','DEF_PIPS_DOUBLE','DEF_AWS','float_small'], 'result':'DEF_BOOL'} 
{'name':'MORE',               'input':['DEF_PIPS_DOUBLE','DEF_PIPS_DOUBLE'], 'result':'DEF_BOOL'}

#DEF_HPRICE_LEVEL
{'name':'MAKE_HPRICE_LEVEL',      'input':['DEF_PRICE','DEF_PIPS_DOUBLE'], 'result':'DEF_HPRICE_LEVEL'}

#DEF_AWS
{'name':'MakeAWS',                'input':['DEF_POINTS'], 'result':'DEF_AWS', 'flags':['singleton']}

#DEF_PIPS_DOUBLE
{'name':'PROP_SIZE',              'input':['DEF_LINE'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'SIZE_CAST',              'input':['DEF_AWS'], 'result':'DEF_PIPS_DOUBLE'}

{'name':'PIPS_MAX',               'input':['DEF_PIPS_DOUBLE','DEF_PIPS_DOUBLE'], 'result':'DEF_PIPS_DOUBLE'} 
{'name':'PIPS_MIN',               'input':['DEF_PIPS_DOUBLE','DEF_PIPS_DOUBLE'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'MATH_AVR_v1',            'input':['DEF_PIPS_DOUBLE','DEF_PIPS_DOUBLE'], 'result':'DEF_PIPS_DOUBLE'}

{'name':'MULT_ABS_v1',            'input':['DEF_SLOPE','DEF_BARS_COUNT'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'DISTANCE',               'input':['DEF_PRICE','DEF_PRICE'], 'result':'DEF_PIPS_DOUBLE'}

{'name':'MA_RANGE_v1',                'input':['ma_range_size','ds','1'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'MA_RANGE_v2',                'input':['ma_range_size','dsD1','1'], 'result':'DEF_PIPS_DOUBLE'}

{'name':'STDDEV8_RANGE_MIN_END',  'input':['10'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'STDDEV20_RANGE_MIN_END', 'input':['10'], 'result':'DEF_PIPS_DOUBLE'}

{'name':'STDDEV8_RANGE_MAX_END',  'input':['10'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'STDDEV20_RANGE_MAX_END', 'input':['10'], 'result':'DEF_PIPS_DOUBLE'}

{'name':'STD_DEV_4_D1',           'input':['1'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'STD_DEV_8_D1',           'input':['1'], 'result':'DEF_PIPS_DOUBLE'}
{'name':'STD_DEV_20_D1',          'input':['1'], 'result':'DEF_PIPS_DOUBLE'}


Рассмотрим простую функцию MORE_I

{'name':'MORE_I', 'input':['DEF_PRICE','DEF_PRICE','invert'], 'result':'DEF_BOOL', 'price':4}

Эта функция принимает два параметра цены (и вспомогательный параметр invert, на него внимание можно не обращать). Возвращает она булевое значение. Параметр price означает некую абстрактную сложность данной функции, задумывалась для контроля сложности всей логики каждого робота.

А вот здесь возникает неплохая олимпиадная задачка: необходимо из исходных функций собрать все возможные варианты логик с заданной сложностью и типом результата. Под логикой следует понимать выражение типа F(X)->Y.

Пример — мы хотим функцию принятия решения о входе в длинную позицию. Нам нужно булевое решение — DEF_BOOL, тогда возможные варианты следующие:

return MORE_I(_CLOSE(__value(1)), _HIGH(__value(1)), invert);//1
return MORE_I(_CLOSE_D1(1), _HIGH(__value(1)), invert);//2
//__value сообщает о типе “1” но его можно убрать.
//PS также желательно понимать могут ли в данной функции быть одинаковые параметры. 

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

Задача алгоритма — сгенерировать функцию, которая будет возвращать DEF_BOOL. Нотация выражения LISP-подобная: [Function Name, param1, param2...]. Параметры, которые начинаются с DEF, являются типом. Выражение в котором есть такой параметр не является окончательным, требует уточнения. В нотации не указывается тип возвращаемого значения за ненадобностью.

  1. Давайте создадим пул таких выражений, где мы их и будем генерировать.
  2. Проверяем нет ли в нашем пуле функции без параметров требующих уточнения. Если есть, выбираем его и возвращаем как результат. Если нет продолжаем.
  3. Выбираем случайно одно из следующих возможных действий — добавить в пул еще одну функцию(4) или заполнить в существующей неуточненные параметры(5).
  4. Добавить новое выражение. Поскольку нам нужны только функции которые будет возвращать тип DEF_BOOL, выбираем все такие функции из списка СП1. Теперь выбираем случайную функцию и записываем ее в пул в виде [‘IS_INSIDE’, ‘DEF_HPRICE_LEVEL’, ‘DEF_PRICE’].
  5. Расширяем существующую функцию. В функции IS_INSIDE два параметра требуют уточнения. Ищем функцию которой можно заполнить параметр DEF_PRICE в СП1.
    Получаем ['IS_INSIDE', ‘DEF_HPRICE_LEVEL’, ['MA_HI_I_v2', '8', 'dsD1', '1', 'invert']].
  6. Возвращаемся к пункту 2.

Результатом алгоритма будет подобное выражение

['IS_INSIDE',
 ['MAKE_HPRICE_LEVEL',
  ['MA_CLOSE_v2', '3', 'dsD1', '1'],
  ['STDDEV8_RANGE_MAX_END', '10']],
 ['MA_HI_I_v2', '8', 'dsD1', '1', 'invert']]

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

Это третья реализация алгоритма, первые два были не столь удачны. Весьма полезно было ознакомиться с 4-м томом Кнута, а именно главой 7.2.1.6 Генерация всех деревьев. Если нужна будет улучшенная версия, обязательно перечитаю ее снова. Недостатками этого алгоритма является:

  1. Надо убедиться что СП1 способен порождать выражения в нужном количестве и многообразии. Для этого у меня просто существует тест, который показывает что из 10000 сгенерированных функций 90% являются уникальными.
  2. Также не ясно какое распределение базовых функций в выражении.
  3. Хотелось бы знать какое количество различных функций может порождать конкретный список базовых функций.
  4. PS. Это, кстати, одно из тех мест системы, где мы заменили всю силу аналитического ума человека на простую функцию Random(). Человек который создает робота уже должен знать ответ на вопрос Как? этот робот будет работать и Почему. ГА здесь просто выполняет роль оптимизированного полного перебора.

Трансляция в конечную форму


Далее это LISP-подобное выражение превращается в листинг на языке SadLobster, где каждое неделимое выражение — это новая переменная. Логически выражение остается тем же.

DEF_BOOL boolA_001(bool invert) {
        DEF_PRICE var_2 = MA_HI_I(8, dsD1, 1, invert);
        DEF_PIPS_DOUBLE var_1 = STDDEV8_RANGE_MAX_END(10);
        DEF_PRICE var_0 = MA_CLOSE(3, dsD1, 1);
        DEF_HPRICE_LEVEL var_3 = MAKE_HPRICE_LEVEL(var_0, var_1);
        DEF_BOOL var_4 = IS_INSIDE(var_3, var_2);
        return var_4;
}

SadLobster это не Haskell c чистыми функциями


Хотя я к этому стремился. Одна из проблем которые стоят при создании языка — обработка ошибок. Сразу возникло желание применить механизм эксепшенов, но MQL их не поддерживает. Самая частовозникаемая проблема — неудачно созданный объект. Идеально было бы использовать nil значения, не будем усложнять раньше времени. Это можно улучшить в следующих версиях. А в текущей реализации просто проверяется валидный ли объект, если нет то функция немедленно завершается. Этим занимается макрос типа CHECK_LINE_OR_FALSE.

DEF_PERIOD var_1 = makePeriodSinceLastDay(ds);
DEF_POINTS var_0 = GetZZPoints(5, ds, 0);
DEF_LINE var_3 = MAKE_SUPPORT(var_0, var_1, 4, !invert); CHECK_LINE_OR_FALSE(var_3);

Оптимизация выражений


Рассмотрим вариант когда выражение выглядит так:

['IS_INSIDE',
 ['MAKE_HPRICE_LEVEL',
  ['GET_MEDIAN_CLOSE_PRICE', ['makePeriodToday', 'ds'], 'ds'], //1
  ['MA_RANGE_v2', '7', 'dsD1', '1']],
 ['GET_MEDIAN_CLOSE_PRICE', ['makePeriodToday', 'ds'], 'ds']] //2

Выражения 1 и 2 одинаковые. После транслирования и выделения переменных, var_2 используется в обоих местах и никакого дублирования кода.

 DEF_PIPS_DOUBLE var_1 = MA_RANGE(7, dsD1, 1);
 DEF_PERIOD var_0 = makePeriodToday(ds);
 DEF_PRICE var_2 = GET_MEDIAN_CLOSE_PRICE(var_0, ds);
 DEF_HPRICE_LEVEL var_3 = MAKE_HPRICE_LEVEL(var_2, var_1);
 DEF_BOOL var_4 = IS_INSIDE(var_3, var_2);

Разработка требует инфраструктуры


Я хотел создать очень робастную базу для конструирования роботов. Разбирая примеры заказов АТС на фриланс бирже, я встраивал новые возможности/требования из ТЗ в общую систему. Так я старался расширить разнообразие в поведении роботов, потому как разнообразие в кодовой базе могло вести к созданию одних и тех же алгоритмических паттернов.

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

  1. Логировать данные в базу во время работы ГА
  2. Достать из базы и обработать
  3. Отобразить графически при помощи mathplotlib

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



Пару слов о производительности


Тестирование очень быстрое по нескольким причинам:

  • Все роботы компилируются в машинный код.
  • Тестирование запускаются многопоточно.
  • Распараллелен даже процесс линковки.
  • Из тестера стратегий урезано много проверок.
  • Используется кеширование для тяжелых функций
  • Тестирование роботов очень грубое, тут нет скальперов или HFT, анализ происходит на часовых графиках.
  • Я использовал процессор на 12 потоков с разгоном до 4GHz Intel Core i7-5820K для тестирования.

Как это работает?


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

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

Хочу уточнить что в работах не используется Money Management и торговля ведется одним и тем же минимальным объемом.



158$ средняя прибыль в месяц при обучении, 21$ — средняя прибыль в течении следующих 12 месяцев. Результаты балансируют около нулевой прибыльности плюс погрешность. С другой стороны можно сравнить со случайным роботом, который просто будет терять на спреде. Не стоит забывать что игра на бирже — это игра с отрицательной суммой. На другом периоде обучения скорее всего результаты будут иные.

Хэпиэнда не будет


Получилось заставить ГА создавать роботов с определенной задачей. Этот проект расширил мое понимание и экспертизу в описанной выше теме. И тут случилось страшное — цель проекта достигнута. Проект для генерации роботов готов. Эта статья подводит черту по проделанной работе.

Вывод хочу разделить на два пункта


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

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

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

И самое главное — я вижу будущее этого проекта в формате песочницы для развития ИИ в области написания алгоритмов.

С удовольствием отвечу на ваши вопросы, предложения и комментарии.

Спасибо за ваше время.
Поделиться с друзьями
-->

Комментарии (22)


  1. babylon
    02.12.2016 21:45
    +4

    Непонятно, но интересно.


  1. hd_keeper
    02.12.2016 22:18
    +3

    Непонятно, сколько денег в итоге удалось заработать.


  1. wlbm_onizuka
    02.12.2016 23:48
    +6

    Если-бы удалось что-то заработать, не было-бы этой статьи, полагаю)


  1. kesn
    03.12.2016 09:37
    +1

    Пацаны, кто-нибудь хоть что-то понял?


  1. mezastel
    03.12.2016 11:38
    -3

    Автор что-то непонятное написал, но пользуясь случаем, оставлю ссылку на нечто более серьезное — мой бесплатный курс по схожей теме.


  1. polarnik
    03.12.2016 11:45
    +1

    Правильно понимаю, что есть внешняя система для разработки роботов на языке MQL, предлагающим набор функций для получения предметных знаний о работе биржи. Вы воссоздали эту систему и научились самостоятельно генерировать и обучать торговых роботов. И создали свой язык SadLobster для описания роботов, который транслируется в MQL/C++, и в результате роботы тестируются быстро, а потом они же используются на внешней площадке?

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

    Из Вашего опыта также выделю полезные знания о фитнес функции. Сложном критерии оценки качества алгоритма.


    1. snowman647
      03.12.2016 12:30

      Да, вы все верно поняли.


  1. polarnik
    03.12.2016 12:07

    Напишите пожалуйста, как правильно скрещиваются функции?
    Функция отражает некое правило, которое влияет на решение покупать или нет (результат типа BOOL, согласно статье). И как происходит скрещивание, какие типы или виды возможны?

    Предполагаю, что если есть два робота, принимающие решение покупать акции ОАО Тыква или нет, то скрещивание может быть таким.

    Первый робот:
    — Есть свободные деньги?
    — ОАО Тыква является прибыльной компанией согласно последнему отчёту?
    — Лето было урожайным в регионе-поставщике?

    Второй робот:
    — Есть свободные деньги?
    — ОАО Тыква является прибыльной компанией согласно последнему отчёту?
    — Была засуха в регионе-покупателе?

    Результат скрещивания:
    — Есть свободные деньги?
    — ОАО Тыква является прибыльной компанией согласно последнему отчёту?
    — Лето было урожайным в регионе-поставщике или была засуха в регионе-покупателе?


    1. snowman647
      03.12.2016 13:12
      +1

      Скрещивание — процесс случайный, поэтому в результате могут получиться такие варианты:
      — Есть свободные деньги?
      — ОАО Тыква является прибыльной компанией согласно последнему отчёту?
      — Лето было урожайным в регионе-поставщике или была засуха в регионе-покупателе?


      И еще, я бы рассмотрел варианты где правил становится больше или меньше:
      — Есть свободные деньги?
      — Была засуха в регионе-покупателе?


      — Есть свободные деньги?
      — ОАО Тыква является прибыльной компанией согласно последнему отчёту?
      — Лето было урожайным в регионе-поставщике и
      — Была засуха в регионе-покупателе?


      Кроме и/или можно включить not, xor, тогде вариантов станет больше.


      Если функции небольшие, как в примере, 2-4 переменных, то результат скрещивания близок к результату мутаций.
      Еще я использовал скрещивание на уровне модулей, а не функций, там это более эффективно, потому что идея скрещивания в том, чтоб оставить части программы нетронутой. Это когда два разных модуля принимают различные решения и они взяты с разных родителей.


      Пример:
      — Первый модуль — покупать или нет?
      — Второй модуль — сколько покупать?


  1. wikipro
    03.12.2016 13:12

    «MetaTrader (сервер) содержит технические возможности, которые могут использоваться брокерами для повышения своей доходности за счёт выполнения операций по худшим для клиента ценам» — т.е. на сервере брокер настраивает «проскальзывание» которое я так понимаю вносит в принципе не предсказываемое нейросетью искажение в торговые тренды, как Вы это обходите?
    2. можно ли использовать QUIC от сбербанка? или с этим какие то технические проблемы


    1. snowman647
      03.12.2016 13:24

      1. Никак не обхожу.
      2. Можно скорее всего, но MT удобнее для моих целей.
        Цель в том, чтоб убедиться, что можно заставить машину создать алгоритм


    1. mkll
      07.12.2016 18:43

      1. Это справедливо для форекс-«кухонь». При использовании MT брокерами на FORTS таких манипуляций вряд ли стоит опасаться. И, кстати, не факт, что адаптированный для FORTS сервер MT вообще обладает такими возможностями — в отличие от форексе, на бирже это просто никому не нужно.

      2. По ответу автора я бы предположил, что он не имел дел с QLua и, тем более, QPILE.


  1. DRDOS
    03.12.2016 13:28

    Вы бы определились, вам нужно описать как вы делали робота. Или принципы построения «генетических роботов». Принципа работы я не увидел. Я не настолько разбираюсь в С++, что бы из кода понять ход ваших мыслей. Из статьи не видно не результатов тестирования, ни толкового отчета по «просадкам».
    Ни конкретно на каких инструментах тестировался робот.
    Только голый текст, из серии «вот я попробовал и получилось».
    А «метатрейдер» вообще используют на лохотроне, под названием Форекс


    1. mkll
      07.12.2016 18:50

      MT используют и на FORTS тоже. Отзывы тех, кто использовал — отличные.


  1. eu-gen
    03.12.2016 14:04

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

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

    Как вы думаете, возможно ли создание такими методами принципиального нового улучшения данного алгоритма в разумное время (которое исчисляется не миллионами лет)? Теоретически это возможно, но на практике мне почему-то кажется что роботам нужно будет «Показать, рассказать, а потом самому за них все сделать»


    1. snowman647
      03.12.2016 15:25

      Мне бы хотелось чтобы это было возможно. Думаю так и есть.
      Вот такой план получения новых алгоритмов:


      1. Воспроизвести создание такого алгоритма через ГА
      2. Менять начальные условия, что даст доступ к созданию всего класса алгоритмов этого типа
      3. Расширять базу и условия, что расширит класс находимых алгоритмов.

      Первый пункт вызывает много вопросов. Попробую ответить.
      Если алгоритм довольно сложный, и интуитивно непонятно как его создать в одночасье, занимаемся декомпозицией. Далее выстраиваем такие условия, чтоб можно было воссоздать самые мелкие функции, затем из них посложнее и так далее. Это и есть "показать и рассказать", от этого не уйти.
      Принципиально новый алгоритм не возникнет в вакууме, поэтому мы должны постоянно вбрасывать туда новые и новые задачи.


      По аналогии с эволюцией первого порядка, эволюция второго порядка тоже должна быть способна на многое. Но это все манипуляции с синтаксисом. Семантику в результаты таких генераций прийдется вкладывать человеку. И когда человек найдет семантически что-то новое для себя, это можно считать успехом.


  1. slonoslon
    03.12.2016 19:12

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

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


  1. GiperTangens
    07.12.2016 18:44

    Важны лишь предикторы, иначе система не будет профитной…


  1. delpasso
    09.12.2016 18:13

    Интереснейшая идея на мой взгляд. Даже если система и не профитная.
    Если это открытый проэкт с удовольствием бы поучаствовал
    Спасибо за статью!


  1. GiperTangens
    10.12.2016 17:34

    Для всех кто интересуется вот интересные ссылки: вот и вот и еще


    1. wlbm_onizuka
      10.12.2016 22:03

      криво запостилось, ток первая ссылка доступна. надо починить)


  1. GiperTangens
    11.12.2016 19:04

    извините… исправлено ... вот — это главная статья по ГА, и еще… очень увлекательные статьи