_sk_
_sk_ личный блог
31 марта 2020, 13:37

QLua: формирование свечных данных для робота

Поделюсь своим опытом, который может быть полезен начинающим алготрейдерам, пишущим своего робота на 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
9 Комментариев
  • Пафос Респектыч
    31 марта 2020, 13:59
    Это как-то помогает написать робота, который будет зарабатывать?
    • Иван Смирнов
      31 марта 2020, 14:17
      Пафос Респектыч, для тех, кто с мозгами и «в теме»-ОЧЕНЬ помогает. Автору-спасибо!
  • Владимиров Владимир
    31 марта 2020, 14:37
    Вы написали: «При завершении работы QLua-скрипта необходимо закрыть datasource-объект командой Close». Можете объяснить — зачем? Если скрипт остановлен, зачем в нем предусматривать эту команду?
      • Владимиров Владимир
        31 марта 2020, 14:59
        _sk_, Понятно. Честно говоря, не делаю это никогда. Хотя в плане бережного отношения к ресурсам, может быть и имеет смысл…
  • Paulmarko
    26 июля 2020, 20:36
    А есть алгоритм как найти последние два максимум и два минимума свечей. Мы получается движемся от текущего времени в прошлое  и как только данные найдены, возвращаем 4 значения. H1, H2, L1, L2.
  • AmiGator
    24 июля 2021, 03:14
    Наверное это очень хороший код, но на этапе
    dataSources[secCode] = ds
    Появляется ошибка
    attempt to index a nil value (global 'dataSources')

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

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