SkillAgentSearch skills...

QuikMultiBridge

Мост между Lua и Python для написания роботов и индикаторов для торгового терминала ARQA QUIK на Python

Install / Use

/learn @eSKond/QuikMultiBridge
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

QuikMultiBridge

Мост Lua<->Python для написания плагинов для терминала ARQA QUIK на Python. Зачем ещё? Ну, просто данная реализация соответствует моему представлению о прекрасном. Вот некоторые особенности, некоторые уже готовы, некоторые в работе:

  • Написан на C++ с использованием библиотеки Qt
  • Питон загружается через соответствующие C-шные интерфейсы, то есть интерпретатор становится частью QUIK
  • Таки позволяет добавлять собственный интерфейс, но я этим пока не занимался. Но если есть желание можете реализовать сами, главное помните, что окна должны создаваться внутри qmbMain после создания QCoreApplication. Ну и интерфейс всё-же желательно писать на кьюте. Впрочем если в питоновской части откроете matplotlib да или тот же PyQt - тоже пожалуйста. Но я бы на устойчивость при этом не ставил.
  • Не имеет встроенную консоль в которую выводится весь вывод из print - мне это просто не нужно, я вполне уже пообвыкся с DebugView. Но добавить - не проблема, см пункт выше
  • Подгружает Python-овскую venv
  • !!! Собирается единожды, после чего не требует пересборки при изменениях АПИ, что достигается полностью динамическим разбором параметров и возвращаемых значений
  • Позволяет писать как индикаторы, так и роботов. Да, вот здесь пришлось повозиться - одновременно несколько роботов да ещё и индикаторы никак не хотели работать. Питоновский GIL выкинул на помойку, сделал собственное переключение между тредами мьютексом. Ниже опишу некоторые важные детали.

В принципе, всё работает. Выжимать что-то ещё из питона не буду, для себя я скорее буду писать сервер на C++ под мои нужды. С numpy и pandas проблем больше нет - удалось решить. Утечек тоже не наблюдаю. Можно пользоваться.

Как писать скрипт робота

  1. Инициализация бриджа из lua
require "QuikMultiBridge"

bridgeConfig = {
    venvPath="c:\\Work\\QuikMultiBridge\\PythonQuik\\tstvenv",
    bridgeModule="qbridge",
    scriptPath="c:\\Work\\QuikMultiBridge\\PythonQuik\\pyRobo.py",
    eventLoopName="main"
    };

initBridge("Python", bridgeConfig);

Что здесь происходит:

  • подключаем dll (можно переименовать её, но там нужно тогда немного иначе загружать, так что смысла не вижу)
  • готовим конфиг, в котором указываем путь к venv, имя модуля, как мы к нему будем обращаться из Python, путь к скрипту, lua метод, который будет у нас основным потоком (об этом ниже)
  • инициализируем бридж указывая что нужно загрузить плагин Python (там заложено несколько вариантов включая R, Qt remote objects, Qt server, но пока есть только Python) и передаём ему конфиг.
  1. Открываем документацию от ARQA "Интерпретатор Lua" и пишем скрипт на Python как если бы мы писали его на Lua с небольшими отличиями:
  • сразу по завершении колбека указанного в eventLoopName (main в примере) бридж самоликвидируется и подчищает за собой.
  • для вызова методов quik мы используем метод бриджа invokeQuik

Но, лучше, давайте посмотрим пример скрипта Python:

import qbridge
from datetime import datetime


ds = None
dsSize = None
dsWasChecked = False
quitRequest = False
updateOutEnabled = True


def dsUpdateCallback(candleIndex):
    global ds, updateOutEnabled
    if updateOutEnabled is True:
        print("C({:d}): {:.2f}".
              format(candleIndex,
                     qbridge.invokeQuikObject(ds, "C", [candleIndex])))
        updateOutEnabled = False


def bridgeMain():
    global ds, dsSize, dsWasChecked, quitRequest, updateOutEnabled

    print("main started")
    qbridge.invokeQuik("PrintDbgStr", ["main started+"])
    while quitRequest is False:
        print(datetime.now().strftime("%Y.%m.%d %H:%M:%S"))
        if ds is None and dsWasChecked is False:
            print("Let create datasource")
            res = qbridge.invokeQuik("CreateDataSource", ["TQBR", "SBER", 1])
            print("data source created: {:d}".format(res))
            if res is not None:
                ds = res
                dsSize = qbridge.invokeQuikObject(ds, "Size", [])
                qbridge.invokeQuikObject(ds, "SetUpdateCallback",
                                         [dsUpdateCallback])
                print("size of DS:{:d}".format(dsSize))
                dsWasChecked = True
        if dsSize is not None:
            if dsSize > 0:
                dsSize -= 1
        updateOutEnabled = True
        qbridge.invokeQuik("sleep", [1000])
    # закрываем
    qbridge.invokeQuikObject(ds, "Close", [])
    # удаляем
    qbridge.deleteQuikObject(ds)
    # очищаем
    ds = None
    dsSize = None


def OnStop(flg):
    global quitRequest
    if flg == 1:
        print("Stopped from dialog")
    else:
        print("Stopped on exit")
    quitRequest = True
    return 10000


if __name__ == '__main__':
    print("Hello from python!")
    qbridge.registerCallback('OnStop', OnStop)
    print("Callback OnStop registered")
    qbridge.registerProcessEventsCallback(processBridgeEvents)
    print("ProcessEventsCallback registered")

import qbridge - тут мы импортируем модуль бриджа с именем, который мы ему задали в конфигурации

Дальше инициализируем глобальные переменные:

  • ds: наш DataSource
  • dsSize: размер ds. Это пример, необходимость его в глобальном пространстве обуславливается логикой робота
  • quitRequest: это сигнал на выход. Вообще для этого есть отдельный метод но его лучше вызывать из processEvents (см. ниже)
  • updateOutEnabled: ну такой вот пример, кому не нравится - напишите лучше :)

Обращаю внимание, что с глобальными переменными нужно обращаться осторожно, я не стал разносить каждый экземпляр по разным сабинтерпретаторам, поэтому у них получилось одно пространство имён. Это легко решается если упаковать весь ваш код в классы. С функциями такой проблемы нет. Почему так? Ну это какая-то странная фишка питона, не знаю, в общем.

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

Метод bridgeMain по сути является тем самым главным циклом main, как вы бы его писали на lua. В каждом цикле мы выводим дату/время и затем начинается магия. Если источник данных ещё не создан, то создаём его вызовом

qbridge.invokeQuik("CreateDataSource", ["TQBR", "SBER", 1])

qbridge.invokeQuikObject(ds, "Size", []) - получаем размер и затем устанавливаем колбек на обновление:

qbridge.invokeQuikObject(ds, "SetUpdateCallback", [dsUpdateCallback])

В этом же методе можно увидеть пример вызова метода Close через invokeQuikObject и удаления объекта с deleteQuikObject

Ну и после завершение этой функции данный бридж завершается

Как видите, мы не закладываемся на сигнатуру вызова, вместо этого разработчик сам смотрит что и как передавать по документации. Параметры всегда передаются как массив (list в терминах Python), даже если в массиве один элемент. То, что в lua называется таблицами передаётся как dict

Функция OnStop ничем не примечательна, кроме способа завершения скрипта установкой переменной quitRequest в True.

В блоке if (стандартная фишка Python, но в принципе можно и без if) мы делаем некоторую подготовительную работу:

  • регистрируем колбэки - OnStop и main

То есть бридж предоставляет только 5 методов:

  • registerCallback
  • invokeQuik
  • invokeQuikObject
  • deleteQuikObject
  • getQuikVariable

В принципе, мы уже так или иначе рассмотрели все методы, кроме getQuikVariable. Этот метод позволяет прочитать (в одну сторону из луа в питон) объект из инициализирующего скрипта lua. Это нужно, в первую очередь, для написания индикаторов на питоне, потому-что им нужна таблица Settings определенного формата Я решил не заморачиваться - раз уж есть инициализирующий lua скрипт, то пусть и Settings будут там же, а из питона мы их прочитаем с помощью getQuikVariable

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

Давайте посмотрим пример реализации MA на питоне:

Инициализирующий Lua скрипт:

require "QuikMultiBridge"

Settings = {
    Name = "SimPyMA",
    mode = "C",
    period = 5,
    line = { {
        Name = "Python Moving Average",
        Color = RGB(90, 110, 200),
        Type = TYPE_LINE,
        Width = 1
        }
    }
};

function Init()
    PrintDbgStr("Prepare bridge config...")
    indBridgeConfig = {
        venvPath="c:\\Work\\QuikMultiBridge\\PythonQuik\\tstvenv",
        bridgeModule="iqb",
        scriptPath="c:\\Work\\QuikMultiBridge\\PythonQuik\\pyIndicator.py",
        eventLoopName="pyOnDestroy"
    }

    PrintDbgStr("Call initBridge")
    initBridge("Python", indBridgeConfig)
    PrintDbgStr("initBridge call finished")

    PrintDbgStr("Call pyInit")
    if pyInit then
        return pyInit()
    end
    return 1
end

function OnCalculate(indx)
    if pyOnCalculate then
        return pyOnCalculate(indx)
    end
    return nil
end

function OnDestroy()
    if pyOnDestroy then
        PrintDbgStr("Call pyOnDestroy")
        pyOnDestroy()
    end
end

На что обратить внимание:

  • все колбеки определены в луа. Питон тоже регистрирует колбеки с префиксом 'py', но мы их вызываем явно из луа с предварительной проверкой, что колбек инициализирован
  • Инициализация бриджа происходит в функции Init - это позволяет не загружать питон до того, как он действительно понадобится (из окна списка индикаторов)
  • eventLoopName определён как pyOnDestroy - как мы помним особенность eventLoopName: после его завершения мы подчищаем за собой. Поэтому несмотря на то, что это не eventLoop как в роботе, мы используем pyOnDestroy как имя для event loop.

Теперь сам индикатор на питоне:

import iqb


funName = 'C'
arr = []
arrsum = 0
period = 10


def Init():
    print("Init")
    return 1


def OnCalculate(idx):
    global funName, arr, period, arrsum
    print("OnCalculate")
    cexist = 
View on GitHub
GitHub Stars7
CategoryDevelopment
Updated1y ago
Forks4

Languages

C++

Security Score

60/100

Audited on May 8, 2024

No findings