Як створити торгового робота з допомогою генетичного програмування



Доброго часу доби. У цій статті розповім про створення системи, в якій генетичні алгоритми пишуть роботів. В теорії ці роботи могли б торгувати на біржі.

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

Проект виник з бажання спробувати на практиці генетичне програмування. Першим варіантом було створювати бота до якої-небудь грі, але я зупинився на торгових роботах, де біржа теж свого роду гра.

Ця стаття має на увазі що ви знайомі з поняттям генетичні алгоритми або генетичне програмування. А також, що роблять торгові роботи.

З чого б почати?
Я почав з вивчення платформи для створення роботів Metatrader 5. Мова 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':'ВІДСТАНЬ', '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)->.

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

Хэпиэнда не буде
Вийшло змусити ГА створювати роботів з певним завданням. Цей проект розширив моє розуміння і експертизу в описаній вище теми. І тут сталося страшне — мета проекту досягнута. Проект для генерації роботів готовий. Ця стаття підводить риску за виконану роботу.

Висновок хочу розділити на два пункти
Суб'єктивний — по ходу роботи назріло безліч варіантів того, що можна було б перевірити в рамках даної системи, для чого вона і створювалася. Наприклад:

  • Використовувати випадкові дані, або не випадкові, подивитися наскільки система обучаема виразним паттернам.
  • Розширити арсенал базових логік на порядки.
  • Запустити навчання на всіх доступних даних відразу.
  • Запустити експеримент у вигляді справжньої еволюції, де кожну ітерацію на вхід подаються нові дані без повторень.
Об'єктивний — технічний аналіз інструменту це як водіння авто дивлячись у дзеркало заднього виду. Простого патерну для торгового робота знайти не вдалося. Без повної моделі ринку не ясно чому працюють ті чи інші роботи, і коли це припиниться.

І найголовніше — я бачу майбутнє цього проекту у форматі пісочниці для розвитку ШІ в області написання алгоритмів.

З задоволенням відповім на ваші запитання, пропозиції та коментарі.

Спасибі за ваш час.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.