######################################################################################################################## # 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')
Скачать Jatotrader можно здесь. Как получить ключ в этом видео. Как подключиться к КВИКУ смотри здесь. С 8-м Квиком пока не работает, доделываю, по плану после 25 мая — биржа вводит 19-ти значные заявки.
Подписаться на мой канал можно здесь в ютьюбе.
Можно например extractSignals. Ну или checkForSignals согласно комментарию над функцией. Короче вы поняли.
Ну и будем честными — считать закрытие свечи как цену сделки (ну я так код понял, давно питон не трогал) для относительно частых сигналов, вносит такую погрешность, что финальный симулированный результат может быть больше похож на шум.
Я наверное не правильно объяснил, ф-я simulate только распределяет сигналы по барам. Сами сигналы формируются в функциях SignalSell и SignalBuy. А здесь вы правы, закрытие-открытие свечи нельзя считать исполнением сделки. Это упрощение сделано лишь для проверки гипотез и не является зеркалом реальной торговой системы. Правда, учитывая среднее количество сделок менее 9 на одну торговую сессию и то, что реальная заявка может быть исполнена как хуже, так и лучше цены закрытия бара, такое допущение является вполне адекватным. В следующих версиях симулятора я могу добавить механизм выставления и исполнения ордеров даже с учетом «латенси», как это сделано в Jatotrader.