Евгений Шибаев
Евгений Шибаев личный блог
27 апреля 2020, 19:12

Искусственный трейдер. Часть 3. Или ТСЛаb в 20 строк кода.

Надеюсь, все живы и здоровы!
Предупреждаю сразу — текста будет больше чем когда кОда (сам код в конце топика).
Перед тем как перейти к созданию алгоритмов машинного обучения, напишем код для тестирования стратегий и отображения результатов.
Мне нужно: описать логику сигналов на покупку и продажу, затем эти сигналы передать симулятору, который в течение конкретной торговой сессии будет показывать на графике точки, соответствующие этим сигналам, а также рассчитывать изменение прибыли и текущей позиции в каждый момент времени. Данные должны загружаться в хронологическом порядке в цикле по торговым сессиям. После завершения обработки нужно создать итоговый график «эквити» по дням, на графике видеть значения максимальной прибыли и «просадки» за каждую торговую сессию, максимальный уровень риска (величину открытой позиции), количество совершенных сделок и соотношение убыточных-прибыльных дней. Вроде бы все пока. Короче, нужно по-быстрому написать ТСЛаb.
Назовем его JatoLab, потому, что датасеты можно формировать в терминале Jatotrader на основе тиковых исторических данных по основным ликвидным инструментам, а затем использовать для машинного обучения. На самом деле данные можно брать из любых ваших источников, например 5-минутные графики в формате DATETIME, HIGH, LOW, OPEN, CLOSE, VOL.
Для приготовления JatoLab нам понадобится Питон 3.х (я использую Jupyter Notebook, Anaconda 3).
Итак, «кодим» по-порядку.
Сигнал на продажу представляет собой функцию двух аргументов SellSignal(candles, i) (32...33 строки), где - candles это датафрейм, содержащий свечи конкретной торговой сессии (описание датафрейма можно найти в начале кода), i — это i-я строка датафрейма, она же «свеча» (отсчет ведется с нулевой). Функция возвращает значение True, если на i-м баре возникает сигнал на продажу. Условие, при котором это происходит, может быть любым, например сочетание каких либо параметров датафрейма, паттерн, и даже результат анализа твитов Трампа. Пример условия для сигналов рассмотрим ниже. Функция BuySignal(candles, i) (36...37 строки) написана по аналогии.
Функция simulate (candles) (41...51 строки), аргумент candles. Выполняет цикл по свечам торговой сессии в хронологическом порядке и проверяет выполнение условий возникновения сигналов на покупку или продажу. Возвращает два списка индексов свечей, которые соответствуют сигналам. Например, возвращаемое значение [[14, 35], [28]], означает, что сигналы на покупку были на 14-й и 35-й свечах, а сигнал на продажу на 28-й свече текущего датафрейма.
Функция daylyequity (buys, sells) (56...88 строки) принимает списки индексов сигналов на покупку и продажу, рассчитывает максимальные значения прибыли и «просадки», а также размер позиции по каждому бару внутри торговой сессии. Возвращает «эквити» (список значений прибыли на каждом баре).
Основные функции готовы. Попробуем все проверить на примере фьючерсного контракта RIU9 и наборе 1500 тиков на бар, созданных с помощью Jatotrader. Сигналом на покупку, для примера,  пусть будет пересечение индикатором ОТО уровня -20% сверху вниз (перепроданность), сигналом на продажу пересечение индикатором ОТО уровня +20% снизу вверх (перекупленность). Проверяем «контртрендовый» подход.
Исходные данные можно взять отсюда DATASET. Здесь в сжатом виде (> 100МБ) представлены свечи частотных графиков RIH0, RIU9, RIZ9, SIH0, SIZ9, SRH0, BRH9-BRH0 в нарезках 125, 250, 500, 750, 1000, 1500 тиков на бар. Помимо DATETIME,H,L,O,C,V наборы содержат: DH, DL, DO и DC — максимальное, минимальное значения, значения на открытие и закрытие накопленной маркет-дельты бара. OTO — значение объемно-тикового осциллятора на закрытии бара, BI, SI — интенсивность покупок и продаж (тиков в секунду). BV,SV -объем покупок и продаж, BC, SC -количество покупок и продаж. Сделать собственные «нарезки», например, по времени, эквиобъемные, типа Ренко или по изменению модуля маркет-дельты можно с помощью Jatotrader. Как это делается рассказано в топике «Искусственный трейдер. Часть 1. Подготовка данных для машинного обучения».
Разбор основного кода.
В начале кода укажем путь к папке DATASET. Затем, имя тикера, например: tiker = 'RIU9', метод формирования бара: method = 'TICKS', количество тиков в баре: frame = 1500. Затем запускаем код (CTRL+Enter). Если вы получите ошибку при загрузке файла — значит в наборе нет файлов с именами YYYY-MM-DD_RIU9_TICKS_1500.FRQ В этом случае либо укажите имеющиеся наборы, либо сформируйте через Jatotrader необходимый вам набор данных.
Основной цикл организован по торговым сессиям — один файл — одна торговая сессия. Данные загружаются в датафрейм candles. Для получения значение параметра i-го бара используйте candles['Имяпараметра'][i]. Например candles['H'][15] — это максимальное значение цены 16-го бара (т.к. отсчет с нулевого) . 
После загрузки данных, Питон немного подумает и отобразит графики всех торговых сессий для RIU9 (это 64 рисунка по три графика на каждом), сигналы на покупку и продажу и эквити внутри каждого дня примерно в таком виде:
Искусственный трейдер. Часть 3. Или ТСЛаb в 20 строк кода.
Последним рисунком будет итоговый отчет по всему фьючерсному контракту в виде гистограммы:
Искусственный трейдер. Часть 3. Или ТСЛаb в 20 строк кода.
Хочу сказать что, мы применили сигналы на покупку-и продажу «в чистом виде», не анализируя уровень открытой позиции.
Поэтому сразу в глаза бросается закономерность (ее можно даже назвать «Граалем») — чем выше уровень риска (объем открытой позиции, правая дополнительная шкала) при «контртрендовой» торговле, тем больше будет и размер потерь. 12-я, 14-я, 15-я, 23-я, 29-я, 39-я и 52-я торговые сессии открытая позиция была 5 или более контрактов. Во все эти дни наблюдались серьезные «просадки». И лишь в 63-ю сессию увеличение позиции проходило по тренду.
Искусственный трейдер. Часть 3. Или ТСЛаb в 20 строк кода.
Так что берегите себя и свои депозиты. Не принимайте больше риска, если «что-то пошло не так».

Код на Питоне с подробными комментариями прилагается:
########################################################################################################################
# Jatotrader 3.0 #              JatoLab for Python. Version 1.0. Apr 27 2020, ©2020 by Evgeny Shibaev                                                                                                                                   # 
########################################################################################################################
import numpy as np
import pandas as pd
#from datetime import datetime
import matplotlib.pyplot as plt
from glob import glob

#Исходными данными явлются файлы в формате csv.
#Файлы находятся в папке Jatotrader\DATASET\имятикера\. Каждый файл содержит в себе информацию по одной торговой сессии
#одного тикера с заданным методом формирования баров. Дата сессии и метод формирования баров указаны в имени файла.
#Например, в файле 2020-02-03_RIH0_TICKS_500.FRQ содержится информация за 3 февраля 2020 года по фьючерсному контракту RIH0,
#сформированная из расчета 500 тиков на бар. Превая строка файла - это имена столбцов DATETIME,H,L,O,C,DH,DL,DO,DC,OTO,
#BI,SI,BV,SV,BC,SC. Последующие строки - это бары, идущие в хронологическом порядке. DATETIME-строка в формате
#"ГГГГ-ММ-ДД ЧЧ:ММ:СС", H,L,O,C - максимальная, минимальная цена и цена открытия и закрытия бара. DH и DL - максимальное
# и минимальное значение накопленной маркет-дельты бара, DO и DC - значение маркет-дельты при открытии и закрытии бара.
#OTO - значение объемно-тикового осциллятора на закрытии бара, BI-интенсивность покупок (тиков в секунду),
#SI- интенсивность продаж, BV-объем покупок, SV-объем продаж, BC-количество покупок,SC-количество продаж

#Путь к набору данных - укажите ваш путь к папке DATASET
path = 'C:\\Jatotrader\\DATASET\\'
#Изменяемые параметры частотных графиков
tiker = 'RIU9' #Имя тикера
method = 'TICKS' #Метод формирования бара
frame = 1500 #Размер бара

#Функция сигнала на продажу. Аргументы: датафрейм candles - это свечи с данными за конкретную торговую сессию,
# i - индекс свечи внутри дня. Возвращает значение True, если условие истинно, иначе False.
#Условием сигнала может быть любая комбинация параметров свечей из датафрейма candles. В данном случае, сигналом на продажу
#на i-й (текущей) свече будет являться пересечение индикатором ОТО уровня в 20% снизу вверх (i-1 - индекс предыдущей свечи)
def SignalSell (candles, i):
    return candles['OTO'][i] > 20 and candles['OTO'][i-1] < 20

#По аналогии функции сигнала на продажу.
def SignalBuy (candles, i):
    return candles['OTO'][i] < -20 and candles['OTO'][i-1] > -20

#Функция проверки сигналов на покупку-продажу по всем свечам торговой сессии. Возвращает списки сигналов на покупку и
#продажу.Каждый список состоит из индексов свечей. Например tobuy=[17, 78] означает сигналы на покупку на 17-й и 78-й свече.
def simulate (candles):
    tobuy, tosell = [],[]
    for i in range(len(candles)): #Основной цикл по свечам
        #ВАЖНО!!! ИНДЕКС СВЕЧИ ДОЛЖЕН БЫТЬ НЕ МЕНЕЕ ИНДЕКСА, ИСПОЛЬЗУЕМОГО В SignalBuy И SignalBuy
        #Например, если в функции SignalSell вы используете параметры i-4 свечи, то строка ниже будет такой if i > 3:
        if i > 1:
            if SignalSell(candles, i):
                tosell.append(i)
            if SignalBuy(candles, i):
                tobuy.append(i)
    return tobuy, tosell

#Расчет прибыли и риска (максимального количества контрактов текущей позиции) по барам внутри торговой сессии
#Аргументы: списоки сигналов на покупку и продажу (индексы баров, соответствующих сигналам)
#Возвращает список, соответствующий значениям прибыли (убытка) на каждом баре торговой сессии
def daylyequity (buys, sells):
    equity, risk =[], []
    daymax=-1000000000
    daymin=1000000000
    maxrisk=0
    outind = len(candles)
    sumbuys, qbuys, sumsells, qsells, avgbuys, avgsells = 0, 0, 0, 0, 0, 0
    for i in range(outind): #Цикл по всем свечам торговой сессии
        if i in buys:
            quantb = buys.count(i)
            sumbuys += candles['C'][i] * quantb
            qbuys += quantb
            avgbuys = sumbuys/qbuys
        if i in sells:
            quants = sells.count(i)
            sumsells += candles['C'][i] * quants
            qsells += quants
            avgsells = sumsells/qsells
        avgremain = avgbuys if avgbuys > avgsells else avgsells
        closedprof = (avgsells - avgbuys) * min (qsells, qbuys)
        profremain = (candles['C'][i] - avgbuys) if (qbuys > qsells) else (avgsells - candles['C'][i])
        profit= closedprof + abs(qsells - qbuys)*profremain
        daymax = max (daymax, profit)
        daymin = min (daymin, profit)
        maxrisk = max (maxrisk, abs(qbuys - qsells))
        equity.append(profit)
        risk.append(qbuys - qsells)
    dayproflist.append(equity[-1]) #Закрытие позиции по закрытию торговой сессии
    daymaxlist.append(daymax)
    dayminlist.append(daymin)
    dayrisk.append(maxrisk)
    candles['RISK']= risk
    return equity

#Инициализация переменных
#dayproflist - итоговая прибыль по дням, Инициализация переменных
dayproflist, daymaxlist, dayminlist, dayrisk = [], [], [], []
trades=0
shift=0
#############             Основной цикл по торговым сессиям набора данных          #######################################
#Каждый файл это данные за конкретную торговую сессию одного частотного графика, например RIH0 500 тиков на бар.
files = glob(f'{path}{tiker}\\*_{tiker}_{method}_{frame}.frq')
#Читаем в датафрейм candles из файлов с частотными данными за соответствующую дату с соотв. частотными настройками
for file in files: #Цикл по торговым сессиям
    candles=pd.read_csv(file, header=0, sep=',', skiprows = [1,2])
    clen=len(candles) #Количество частотных свечей
    istart=0 #Индекс первой свечи на графике
    xwidth= min(clen, clen) #Количество свечей, отображаемое на графике
    fig, [axPrice, axOTO, axEq] = plt.subplots(3, 1, figsize=(16, 8)) #Три секции графика с соответствующими осями
    axPrice.set_title(file)
    axPrice.set_ylabel('Price')
    axOTO.set_ylabel('OTO')
    axEq.set_ylabel('Equity')
    axRisk = axEq.twinx() #Дополнительная шкала уровня риска на графике эквити
    axRisk.set_ylabel('Position')
    axDPrice = axPrice.twinx() #Дополнительная шкала изменения цены
    axDOTO = axOTO.twinx()
    for ax in [axPrice, axOTO, axEq, axDOTO]: #цикл по секциям axPrice
        ax.grid(True) #Добавляем сетку в каждую секцию
        ax.set_xlim(xmin=istart, xmax=xwidth) #Задаем границы отображения по шкале X
    t = np.arange(istart, xwidth) #Шкала Х по количеству свечей
    axOTO.plot(t, candles['OTO'][istart:xwidth], linewidth=1) #Рисуем график ОТО 
    axPrice.plot(t, candles['C'][istart:xwidth], color = 'black', linewidth=0.7)
    print(clen, "свечей прочитано")
    #Это просто пример как можно получить столбец изменений параметра от предыдущих значений (изменеия ОТО)
    candles['DOTO']=[0]+[candles['OTO'][i+1]-candles['OTO'][i] for i in range (clen-1)]
    colorsOTO = ['indianred' if x < 0 else 'seagreen' for x in candles['DOTO']]
    axDOTO.bar(t, candles['DOTO'][istart:xwidth], color = colorsOTO, alpha = 0.5)
    #Формируем сигналы на покупку и продажу
    signals = simulate(candles)
    tb=signals[0] #Список индексов свечей с сигналами на покупку
    #Сигналы на покупку на графике
    axPrice.scatter([x+shift for x in tb], [candles['C'][x+shift] for x in tb], marker = 'o', color='g')
    ts=signals[1] #Список индексов свечей с сигналами на продажу
    #Сигналы на продажу на графике
    axPrice.scatter([x+shift for x in ts], [candles['C'][x+shift] for x in ts], marker = 'o', color='r')
    trades += len(tb)+len(ts)+abs(len(tb)-len(ts)) #Количество сделок с учетом закрытия позиции полностью в конце дня
    #Вертикальные линии сигналов по всем секциям графика для визуального контроля
    for s in tb:
        axPrice.axvline(x=s, color='g', alpha=0.35)
        axOTO.axvline(x=s, color='g', alpha=0.35)
        axEq.axvline(x=s, color='g', alpha=0.35)
    for s in ts:
        axPrice.axvline(x=s, color='r', alpha=0.35)
        axOTO.axvline(x=s, color='r', alpha=0.35)
        axEq.axvline(x=s, color='r', alpha=0.35)
    #График эквити внутри торговой сессии    
    de=np.array(daylyequity(tb, ts))
    axEq.plot(t, de, color = 'black', linewidth=0.5)
    axEq.text(t[-1]+1, de[-1], "%.2f" % (de[-1]), bbox={'facecolor':'darkred' if de[-1] < 0 else 'darkgreen', 'pad':3}, color = 'white')
    #График изменения позиции (риска) внутри торговой сессии 
    axRisk.plot(t, candles['RISK'][istart:xwidth], color = 'black', linewidth=0.5)
    axEq.fill_between(t, de, where= (de > 0), facecolor='g', alpha=0.35, interpolate=True)
    axEq.fill_between(t, de, where= (de < 0), facecolor='r', alpha=0.35, interpolate=True)

#Итоговая "эквити"
#Накопленная прибыль по дням
res = 0
eq = []
for d in dayproflist:
    res +=d
    eq.append(res)
fig, ax = plt.subplots(figsize=(16, 8))
axRes = ax.twinx() #Дополнительная шкала накопленной прибыли
axRisk = ax.twinx() #Дополнительная шкала риска по дням
ax.grid(True)
#Прибыль по итогу торговой сессии
x = [x for x in range (len (dayproflist))]
y = [y for y in dayproflist]
color = ['red' if y < 0 else 'green' for y in dayproflist]
ax.bar(x, y, color = color, alpha=0.75, width = 0.65)
axRes.plot(x, eq, color = 'black', linewidth=1)
#Максимальная прибыль в течение торговой сессии
y = [y for y in daymaxlist]
color = ['r' if y < 0 else 'g' for y in daymaxlist]
ax.bar(x, y, color = color, alpha=0.35)
#Минимальная прибыль (максимальный убыток) в течение торговой сессии
y = [y for y in dayminlist]
color = ['r' if y < 0 else 'g' for y in dayminlist]
ax.bar(x, y, color = color, alpha=0.35)
#Максимальный риск (объем открытой позиции) в течение торговой сессии
y = [y for y in dayrisk]
axRisk.bar(x, y, color = 'b', width = 0.3, alpha = 0.35)
#Итоговые показатели
sumeq=np.array(eq)
sumprof=sumeq[-1]
axRes.fill_between(x, sumeq, where= (sumeq > 0), facecolor='g', alpha=0.35, interpolate=True)
axRes.fill_between(x, sumeq, where= (sumeq < 0), facecolor='r', alpha=0.35, interpolate=True)
axRes.text(x[-1]+1, sumprof, "%.2f" % (sumprof), bbox={'facecolor':'g', 'alpha':0.5, 'pad':3})
wins=sum(i > 0 for i in dayproflist)
loss=sum(i <= 0 for i in dayproflist)
days=len(dayproflist)
axRes.set_title(f'Ticker: {tiker} {frame} {method} per bar')
ax.text(0, max(daymaxlist), (f'Win days: {wins} ({round (wins/days*100, 2)}%)   Loss days: {loss} ({round (loss/days*100, 2)}%)   in {days} days. Max risk: {max(dayrisk)} contracts. Trades: {trades}'))
axRes.set_title(f'Ticker: {tiker} {frame} {method} per bar')

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

Код JatoLab ноутбук для Python3.

Скачать Jatotrader можно здесь. Как получить ключ в этом видео. Как подключиться к КВИКУ смотри здесь. С 8-м Квиком пока не работает, доделываю, по плану после 25 мая — биржа вводит 19-ти значные заявки.
Подписаться на мой канал можно здесь в ютьюбе.

Всем здоровья и счастливой самоизоляции!
7 Комментариев
  • kamperman
    27 апреля 2020, 19:28
    Отличный продукт, рекомендую!
  • Ынвестор
    27 апреля 2020, 20:52
    Действительно текста много. Сразу не разберешься. Я правильно понимаю что вы Питон умеете к Квику присоединять?
  • Нувот Вчеранов
    27 апреля 2020, 22:06
    Жду пожеланий по улучшению кода.
    Не называть функцию получения сигнала из датасета simulate.
    Можно например extractSignals. Ну или checkForSignals согласно комментарию над функцией. Короче вы поняли.

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

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн