Поделюсь своим опытом, который может быть полезен начинающим алготрейдерам, пишущим своего робота на QLua.
Внутри QLua есть стандартный способ, которым можно заказать свечные данные. Это делается через функцию CreateDataSource. При этом терминал возвращает все свечи, которые у него есть на момент вызова этой функции, но это может быть не совсем удобно. Вот несколько примеров.
Пример 1. Мы торгуем акции на 30-минутках и при этом не хотим учитывать свечу, которая получается в 9:30 из-за аукциона открытия, и не хотим, чтобы аукцион закрытия портил последнюю свечу дня в 18:30. Хотим только нужные свечи в одном массиве.
Пример 2. Мы торгуем фьючерсы только в дневную сессию, а вечернюю сессию выбрасываем, поскольку наша стратегия в этом случае даёт более приличный график эквити. Хочется иметь «отфильтрованный» свечной ряд.
Пример 3. Мы торгуем американские акции на Санкт-Петербургской бирже и хотим, чтобы время свечей было как в Америке, а не как на бирже, и хотим оставить только основные торги с 9:30 до 16:00 по буржуйскому времени.
Чтобы добиться целей, обозначенных в этих примерах, надо уметь фильтровать свечи, объединять свечи более коротких таймфреймов в свечи нужных нам более длинных таймфреймов, а также менять время свечи, приводя его к нужному часовому поясу. Например, заказывать 5-минутные данные из терминала QUIK, а потом отбрасывать ненужные свечи и объединять оставшиеся в более крупные результирующие свечи.
Все это реализовано в коде Candles.lua, который приводится в конце заметки. Там есть достаточно подробное описание каждой функции в соответствующих комментариях. Вполне по силам разобраться, если изучать вдумчиво и внимательно.
Этот код корректно работает в потоке main, запрашивая данные из терминала в критической секции, реализованной через
грязный хак table.ssort. В дополнение ко всему, мы можем запросить формирование не всех свечей, а только нескольких последних, чтобы сэкономить время на вычислении индикаторов.
Использование Candles.lua происходит примерно так.
0) Импорт модуля Canldes.lua
local Candles = require("util.Candles")
1) Открытие datasource-объекта с помощью функции create. При этом надо указать:
— код класса и код инструмента;
— таймфрейм для свечных данных, из которых будут собираться нужные нам свечи;
— строку «indicativequote» для получения свечей по индикативным ценам или nil, если по обычным;
— время в секундах, которое скрипт будет ожидать при заказе данных.
Пример кода:
local ds, error = Candles.create(classCode, secCode, timeframe, "indicativequote", timeout)
if ds == nil then
logger:error("Cannot create datasource for " .. secCode .. ": " .. error)
else
dataSources[secCode] = ds
if ds:Size() == 0 then
logger:error("No data for " .. secCode)
ds:Close()
dataSources[secCode] = nil
else
logger:info(ds:Size() .. " " .. timeframe .. "-min candles for " .. secCode .. " loaded.")
end
end
2) Получение свечных данных на основании datasource из предыдущего пункта.
Пример кода для СПБиржи, где мы корректируем время с учётом перевода часов (надо дописывать информацию о моментах перевода времени в Америке; последний перевод стрелок часов не учтён) и фомируем 30-минутные свечи:
local HOUR = 3600
--- Перевод московское время -> нью-йоркское время
-- @param dt таблица с датой и временем в Москве
-- @return таблица с датой и временем в Нью-Йорке
local function MSK_NY(dt)
local yyyymmddhh = ((dt.year * 100 + dt.month) * 100 + dt.day) * 100 + dt.hour
local diff = 0
if yyyymmddhh >= 2019110310 then
diff = 8 * HOUR
elseif yyyymmddhh >= 2019031009 then
diff = 7 * HOUR
elseif yyyymmddhh >= 2018110410 then
diff = 8 * HOUR
else
diff = 7 * HOUR
end
return os.date("*t", os.time(dt) - diff)
end
--- Функция для фильтрации свечей основной торговой сессии.
local accept = function(dt)
local hhmm = dt.hour * 100 + dt.min
return 930 <= hhmm and hhmm < 1600
end
-- Запрос свечей.
local candles = Candles.getCandles(ds, MSK_NY, accept, 30)
3) Собственно работа с таблицей candles, которая имеет поля:
— size — количество свечей;
— T — массив, индексируемый от 1 до size, содержащий время свечей;
— O, H, L, C, V — массивы с аналогичной индексацией, содержащие данные по Open, High, Low, Close, Volume.
По этим данным уже можно удобным образом вычислить значения ваших индикаторов, проверить нужные условия и совершать сделки.
4) При завершении работы QLua-скрипта необходимо закрыть datasource-объект командой Close.
Также имеется возможность урезания набора свечей (остаются самые последние в указанном количестве), а также поиск свечи по моменту времени. Детали см. в коде ниже.
Успехов в алготорговле!
Код Candles.lua:
--
-- Формирование свечей по содержимому DataSource с фильтрацией и агрегированием.
--
local Candles = {}
local intervals = {
[1] = INTERVAL_M1,
[2] = INTERVAL_M2,
[3] = INTERVAL_M3,
[4] = INTERVAL_M4,
[5] = INTERVAL_M5,
[6] = INTERVAL_M6,
[10] = INTERVAL_M10,
[15] = INTERVAL_M15,
[20] = INTERVAL_M20,
[30] = INTERVAL_M30,
[60] = INTERVAL_H1,
}
--- Создать объект DataSource и подождать, пока в нём появятся данные, но не более указанного количества секунд.
-- @param classCode код классса
-- @param secCode код инструмента
-- @param timeframe таймфрейм в минутах (1, 2, 3, 4, 5, 6, 10, 15, 20, 30, 60)
-- @param paramName имя параметра или nil, если нужно заказать обычные свечи
-- @param timeout таймаут ожидания данных в секундах
-- @return объект DataSource в случае успешного создания объекта, иначе nil, error
local function create(classCode, secCode, timeframe, paramName, timeout)
local ds, error
if paramName == nil then
ds, error = CreateDataSource(classCode, secCode, intervals[timeframe])
else
ds, error = CreateDataSource(classCode, secCode, intervals[timeframe], paramName)
end
if ds == nil then
return nil, error
end
ds:SetEmptyCallback()
local deadline = os.time() + (timeout or 15)
while os.time() < deadline and ds:Size() == 0 do
sleep(10)
end
if ds:Size() == 0 then
return nil, "No candles for " .. secCode
end
return ds
end
Candles.create = create
local function equalDateTime(dt1, dt2)
return type(dt1) == "table" and type(dt2) == "table"
and dt1.year == dt2.year and dt1.month == dt2.month and dt1.day == dt2.day
and dt1.hour == dt2.hour and dt1.min == dt2.min and dt1.sec == dt2.sec
and (dt1.ms or 0) == (dt2.ms or 0)
end
--- Получить свечи из открытого объекта DataSource.
-- @param ds объект DataSource
-- @param maxSize максимальное количество запрашиваемых свечей или nil, если ограничение не нужно
-- @return таблица с полями size (количество свечей) и массивами T, O, H, L, C, V, индексируемыми от 1 до size
-- и содержащими параметры свечей
local function getRawCandles(ds, maxSize)
local candles = {
size = 0,
T = {},
O = {},
H = {},
L = {},
C = {},
V = {},
}
if ds == nil or ds:Size() == 0 then
return candles
end
table.ssort({ 0, 1 }, function(a, b)
local dsSize = ds:Size()
if maxSize == nil then
maxSize = dsSize
end
local size, offset
if dsSize <= maxSize then
size, offset = dsSize, 0
else
size, offset = maxSize, dsSize - maxSize
end
for i = 1, size do
local j = i + offset
candles.T[i] = ds:T(j)
candles.O[i] = ds:O(j)
candles.H[i] = ds:H(j)
candles.L[i] = ds:L(j)
candles.C[i] = ds:C(j)
candles.V[i] = ds:V(j)
end
candles.size = size
return true
end)
return candles
end
Candles.getRawCandles = getRawCandles
--- Получить свечи из открытого DataSource. При необходимости можно отфильтровать свечи и агрегировать их
-- согласно указанному таймфрейму.
-- @param ds объект DataSource
-- @param timeshift функция, преобразующая таблицу со временем, иначе время не преобразуется
-- @param accept функция фильтрации в зависимости от уже преобразованного времени свечи,
-- иначе будут использованы все свечи
-- @param timeframe таймфрейм в минутах (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60)
-- или nil, если агрегация не нужна
-- @param rawCandlesMaxSize максимальное количество свечей, запрашиваемых в функции getRawCandles,
-- или nil, если запрашиваются все свечи
-- @return таблица с полями size (количество свечей) и массивами T, O, H, L, C, V, индексируемыми от 1 до size
-- и содержащими соответствущие параметры свечей
local function getCandles(ds, timeshift, accept, timeframe, rawCandlesMaxSize)
local rawCandles = getRawCandles(ds, rawCandlesMaxSize)
if rawCandles.size == 0 then
return rawCandles
end
if type(timeshift) ~= "function" then
timeshift = nil
end
if type(accept) ~= "function" then
accept = nil
end
if intervals[timeframe] == nil then
timeframe = nil
end
if timeshift == nil and accept == nil and timeframe == nil then
return rawCandles
end
local candles = {
size = 0,
T = {},
O = {},
H = {},
L = {},
C = {},
V = {},
}
for i = 1, rawCandles.size do
local dt = rawCandles.T[i]
if timeshift ~= nil then
dt = timeshift(dt)
end
if accept == nil or accept(dt) then
dt.min = dt.min - (dt.min % timeframe)
dt.sec = 0
if dt.ms then
dt.ms = 0
end
if dt.mcs then
dt.mcs = 0
end
local j = candles.size
if j == 0 or not equalDateTime(candles.T[j], dt) then
j = j + 1
candles.size = j
candles.T[j] = dt
candles.O[j] = rawCandles.O[i]
candles.H[j] = rawCandles.H[i]
candles.L[j] = rawCandles.L[i]
candles.C[j] = rawCandles.C[i]
candles.V[j] = rawCandles.V[i]
else
candles.H[j] = math.max(candles.H[j], rawCandles.H[i])
candles.L[j] = math.min(candles.L[j], rawCandles.L[i])
candles.C[j] = rawCandles.C[i]
candles.V[j] = candles.V[j] + rawCandles.V[i]
end
end
end
return candles
end
Candles.getCandles = getCandles
--- Урезать набор свечей, оставив не более чем указанное количество последних свечей.
-- @param candles таблица с полями size (количество свечей) и массивами T, O, H, L, C, V, индексируемыми от 1 до size
-- и содержащими соответствущие параметры свечей
-- @param maxSize максимальное количество оставляемых свечей
-- @return исходная таблица candles, если количество свечей меньше или равно maxSize, либо новая урезанная таблица
-- размера maxSize
local function truncate(candles, maxSize)
if candles.size <= maxSize then
return candles
end
local offset = candles.size - maxSize
local T, O, H, L, C, V = {}, {}, {}, {}, {}, {}
for i = 1, maxSize do
T[i] = candles.T[i + offset]
O[i] = candles.O[i + offset]
H[i] = candles.H[i + offset]
L[i] = candles.L[i + offset]
C[i] = candles.C[i + offset]
V[i] = candles.V[i + offset]
end
return {
size = maxSize,
T = T,
O = O,
H = H,
L = L,
C = C,
V = V,
}
end
Candles.truncate = truncate
--- Сравнение таблиц с датой и временем.
-- @param datetime1 первая таблица с датой и временем
-- @param datetime2 вторая таблица с датой и временем
-- @return -1, если datetime1 < datetime2; +1, если datetime1 > datetime2; 0, если datetime1 == datetime2.
local function datetimeComparator(datetime1, datetime2)
local d = datetime1.year - datetime2.year
if d < 0 then return -1 elseif d > 0 then return 1 end
d = datetime1.month - datetime2.month
if d < 0 then return -1 elseif d > 0 then return 1 end
d = datetime1.day - datetime2.day
if d < 0 then return -1 elseif d > 0 then return 1 end
d = datetime1.hour - datetime2.hour
if d < 0 then return -1 elseif d > 0 then return 1 end
d = datetime1.min - datetime2.min
if d < 0 then return -1 elseif d > 0 then return 1 end
d = datetime1.sec - datetime2.sec
if d < 0 then return -1 elseif d > 0 then return 1 end
d = (datetime1.ms or 0) - (datetime2.ms or 0)
if d < 0 then return -1 elseif d > 0 then return 1 end
d = (datetime1.count or 1) - (datetime2.count or 1)
if d < 0 then return -1 elseif d > 0 then return 1 end
return 0
end
--- Найти индекс свечи в массиве по заданному моменту времени, используя двоичный поиск.
-- @param candles таблица со свечными данными
-- @param datetime таблица с искомыми датой и временем свечи
-- @return true, index, если найдена свеча с таким временем, и false, index, если свеча не найдена;
-- в случае, когда свечи нет, значение index определяет место вставки свечи в массив (от 0 до size + 1).
local function searchCandleId(candles, datetime)
local size = candles.size
if size == 0 then
return false, 1
end
local b = size
local T = candles.T
local d = datetimeComparator(T[b], datetime)
if d == 0 then
return true, b
elseif d < 0 then
return false, b + 1
end
local a = 1
d = datetimeComparator(T[a], datetime)
if d == 0 then
return true, a
elseif d > 0 then
return false, a - 1
end
while true do
local c = math.floor((a + b) / 2)
if a == c then
return false, b
end
d = datetimeComparator(T[c], datetime)
if d < 0 then
a = c
elseif d > 0 then
b = c
else
return true, c
end
end
end
Candles.searchCandleId = searchCandleId
return Candles
Появляется ошибка