commit 87425dfe9cd29b6a37b5b55c9c39ddaeffbb1a1f Author: Игорь Чечет Date: Sun Feb 14 15:47:51 2021 +0500 Initial commit diff --git a/Examples/01 - Connect.py b/Examples/01 - Connect.py new file mode 100644 index 0000000..6f9e34f --- /dev/null +++ b/Examples/01 - Connect.py @@ -0,0 +1,33 @@ +from QuikPy import QuikPy as qp # QuikPy = Работа с Quik из Python через LUA скрипты QuikSharp + + +def PrintCallback(data): + """Пользовательский обработчик события""" + print(data) # Печатаем полученные данные + +if __name__ == '__main__': # Точка входа при запуске этого скрипта + # qpProvider = qp.QuikPy() # Вызываем конструктор QuikPy с подключением к локальному компьютеру с QUIK + qpProvider = qp.QuikPy(Host='192.168.1.7') # Вызываем конструктор QuikPy с подключением к удаленному компьютеру с QUIK + print(f'Подключено к терминалу QUIK по адресу: {qpProvider.Host}:{qpProvider.RequestsPort},{qpProvider.CallbacksPort}') + + # QuikPy - Singleton класс. Будет создан 1 экземпляр класса, на него будут все ссылки + # qpProvider2 = qp.QuikPy() + qpProvider2 = qp.QuikPy(Host='192.168.1.7') # QuikPy - это Singleton класс. При попытке создания нового экземпляра получим ссылку на уже имеющийся экземпляр + print(f'Экземпляры класса совпадают: {qpProvider2 == qpProvider}') + + # Проверка соединения + print(f'Терминал QUIK подключен к серверу: {qpProvider.IsConnected()["data"] == 1}') + print(f'Отклик QUIK на команду Ping: {qpProvider.Ping()["data"]}') + + # Сервисные функции + print(f'Дата на сервере: {qpProvider.GetInfoParam("TRADEDATE")["data"]}') + print(f'Время на сервере: {qpProvider.GetInfoParam("SERVERTIME")["data"]}') + msg = 'Hello from Python!' + print(f'Отправка сообщения в QUIK: {msg}{qpProvider.MessageInfo(msg)["data"]}') + + # Просмотр изменений параметров + qpProvider.OnParam = PrintCallback # Текущие параметры изменяются постоянно. Будем их смотреть, пока не нажмем Enter в консоли + + # Выход + input('Enter - выход') + qpProvider.CloseConnectionAndThread() # Перед выходом закрываем соединение и поток QuikPy из любого экземпляра diff --git a/Examples/02 - Accounts.py b/Examples/02 - Accounts.py new file mode 100644 index 0000000..b4de7db --- /dev/null +++ b/Examples/02 - Accounts.py @@ -0,0 +1,125 @@ +from QuikPy import QuikPy as qp # QuikPy = Работа с Quik из Python через LUA скрипты QuikSharp + + +def GetAllAccounts(): + """Получение всех торговых счетов""" + classCodes = qpProvider.GetClassesList()['data'] # Список классов + classCodesList = classCodes[:-1].split(',') # Удаляем последнюю запятую, разбиваем значения по запятой + tradeAccounts = qpProvider.GetTradeAccounts()['data'] # Все торговые счета + moneyLimits = qpProvider.GetMoneyLimits()['data'] # Все денежные лимиты (остатки на счетах) + depoLimits = qpProvider.GetAllDepoLimits()['data'] # Все лимиты по бумагам (позиции по инструментам) + orders = qpProvider.GetAllOrders()['data'] # Все заявки + stopOrders = qpProvider.GetAllStopOrders()['data'] # Все стоп заявки + + # Коды клиента / Фирмы / Счета + for tradeAccount in tradeAccounts: # Пробегаемся по всем счетам + firmId = tradeAccount['firmid'] # Фирма + tradeAccountId = tradeAccount['trdaccid'] # Счет + distinctClientCode = list(set([moneyLimit['client_code'] for moneyLimit in moneyLimits if moneyLimit['firmid'] == firmId])) # Уникальные коды клиента по фирме + print(f'Код клиента {distinctClientCode[0] if distinctClientCode else "не задан"}, Фирма {firmId}, Счет {tradeAccountId} ({tradeAccount["description"]})') + tradeAccountClassCodes = tradeAccount['class_codes'][1:-1].split('|') # Классы торгового счета. Удаляем последнюю вертикальную черту, разбиваем значения по вертикальной черте + intersectionClassCodes = list(set(tradeAccountClassCodes).intersection(classCodesList)) # Классы, которые есть и в списке и в торговом счете + # Классы + for classCode in intersectionClassCodes: # Пробегаемся по всем общим классам + classInfo = qpProvider.GetClassInfo(classCode)['data'] # Информация о классе + print(f'- Класс {classCode} ({classInfo["name"]}), Тикеров {classInfo["nsecs"]}') + # Инструменты. Если выводить на экран, то занимают много места. Поэтому, закомментировали + # classSecurities = qpProvider.GetClassSecurities(classCode)['data'][:-1].split(',') # Список инструментов класса. Удаляем последнюю запятую, разбиваем значения по запятой + # print(f' - Тикеры ({classSecurities})') + if firmId == 'SPBFUT': # Для фьючерсов свои расчеты + # Лимиты + print(f'- Фьючерсный лимит {qpProvider.GetFuturesLimit(firmId, tradeAccountId, 0, "SUR")["data"]["cbplimit"]} SUR') + # Позиции + futuresHoldings = qpProvider.GetFuturesHoldings()['data'] # Все фьючерсные позиции + activeFuturesHoldings = [futuresHolding for futuresHolding in futuresHoldings if futuresHolding['totalnet'] != 0] # Активные фьючерсные позиции + for activeFuturesHolding in activeFuturesHoldings: + print(f' - Фьючерсная позиция {activeFuturesHolding["sec_code"]} {activeFuturesHolding["totalnet"]} @ {activeFuturesHolding["cbplused"]}') + else: # Для остальных фирм + # Лимиты + firmMoneyLimits = [moneyLimit for moneyLimit in moneyLimits if moneyLimit['firmid'] == firmId] # Денежные лимиты по фирме + for firmMoneyLimit in firmMoneyLimits: # Пробегаемся по всем денежным лимитам + limitKind = firmMoneyLimit['limit_kind'] # День лимита + print(f'- Денежный лимит {firmMoneyLimit["tag"]} на T{limitKind}: {firmMoneyLimit["currentbal"]} {firmMoneyLimit["currcode"]}') + # Позиции + firmKindDepoLimits = [depoLimit for depoLimit in depoLimits if depoLimit['firmid'] == firmId and depoLimit['limit_kind'] == limitKind and depoLimit['currentbal'] != 0] # Берем только открытые позиции по фирме и дню + for firmKindDepoLimit in firmKindDepoLimits: # Пробегаемся по всем позициям + secCode = firmKindDepoLimit["sec_code"] # Код тикера + classCode = qpProvider.GetSecurityClass(classCodes, secCode)['data'] + entryPrice = float(firmKindDepoLimit["wa_position_price"]) + lastPrice = float(qpProvider.GetParamEx(classCode, secCode, 'LAST')['data']['param_value']) # Последняя цена сделки + if classCode == 'TQOB': # Для рынка облигаций + lastPrice *= 10 # Умножаем на 10 + print(f' - Позиция {classCode}.{secCode} {firmKindDepoLimit["currentbal"]} @ {entryPrice:.2f}/{lastPrice:.2f}') + # Заявки + firmOrders = [order for order in orders if order['firmid'] == firmId and order['flags'] & 0b1 == 0b1] # Активные заявки по фирме + for firmOrder in firmOrders: # Пробегаемся по всем заявка + isBuy = firmOrder['flags'] & 0b100 != 0b100 # Заявка на покупку + print(f'- Заявка номер {firmOrder["order_num"]} {"Покупка" if isBuy else "Продажа"} {firmOrder["class_code"]}.{firmOrder["sec_code"]} {firmOrder["qty"]} @ {firmOrder["price"]}') + # Стоп заявки + firmStopOrders = [stopOrder for stopOrder in stopOrders if stopOrder['firmid'] == firmId and stopOrder['flags'] & 0b1 == 0b1] # Активные стоп заявки по фирме + for firmStopOrder in firmStopOrders: # Пробегаемся по всем стоп заявкам + isBuy = firmStopOrder['flags'] & 0b100 != 0b100 # Заявка на покупку + print(f'- Стоп заявка номер {firmStopOrder["order_num"]} {"Покупка" if isBuy else "Продажа"} {firmStopOrder["class_code"]}.{firmStopOrder["sec_code"]} {firmStopOrder["qty"]} @ {firmStopOrder["price"]}') + +def GetAccount(ClientCode='', FirmId='SPBFUT', TradeAccountId='SPBFUT00PST', LimitKind=0, CurrencyCode='SUR'): + """Получение торгового счета. По умолчанию, выдается счет срочного рынка""" + classCodes = qpProvider.GetClassesList()['data'] # Список классов + moneyLimits = qpProvider.GetMoneyLimits()['data'] # Все денежные лимиты (остатки на счетах) + depoLimits = qpProvider.GetAllDepoLimits()['data'] # Все лимиты по бумагам (позиции по инструментам) + orders = qpProvider.GetAllOrders()['data'] # Все заявки + stopOrders = qpProvider.GetAllStopOrders()['data'] # Все стоп заявки + + print(f'Код клиента {ClientCode}, Фирма {FirmId}, Счет {TradeAccountId}, T{LimitKind}, {CurrencyCode}') + if FirmId == 'SPBFUT': # Для фьючерсов свои расчеты + print(f'- Фьючерсный лимит {qpProvider.GetFuturesLimit(FirmId, TradeAccountId, 0, "SUR")["data"]["cbplimit"]} SUR') + futuresHoldings = qpProvider.GetFuturesHoldings()['data'] # Все фьючерсные позиции + activeFuturesHoldings = [futuresHolding for futuresHolding in futuresHoldings if futuresHolding['totalnet'] != 0] # Активные фьючерсные позиции + for activeFuturesHolding in activeFuturesHoldings: + print(f'- Фьючерсная позиция {activeFuturesHolding["sec_code"]} {activeFuturesHolding["totalnet"]} @ {activeFuturesHolding["cbplused"]}') + else: # Для остальных фирм + accountMoneyLimit = [moneyLimit for moneyLimit in moneyLimits # Денежный лимит + if moneyLimit['client_code'] == ClientCode and # Выбираем по коду клиента + moneyLimit['firmid'] == FirmId and # Фирме + moneyLimit['limit_kind'] == LimitKind and # Дню лимита + moneyLimit["currcode"] == CurrencyCode][0] # Валюте + print(f'- Денежный лимит {accountMoneyLimit["currentbal"]}') + accountDepoLimits = [depoLimit for depoLimit in depoLimits # Бумажный лимит + if depoLimit['client_code'] == ClientCode and # Выбираем по коду клиента + depoLimit['firmid'] == FirmId and # Фирме + depoLimit['limit_kind'] == LimitKind and # Дню лимита + depoLimit['currentbal'] != 0] # Берем только открытые позиции по фирме и дню + for firmKindDepoLimit in accountDepoLimits: # Пробегаемся по всем позициям + secCode = firmKindDepoLimit["sec_code"] # Код тикера + entryPrice = float(firmKindDepoLimit["wa_position_price"]) + classCode = qpProvider.GetSecurityClass(classCodes, secCode)['data'] + lastPrice = float(qpProvider.GetParamEx(classCode, secCode, 'LAST')['data']['param_value']) # Последняя цена сделки + if classCode == 'TQOB': # Для рынка облигаций + lastPrice *= 10 # Умножаем на 10 + print(f'- Позиция {classCode}.{secCode} {firmKindDepoLimit["currentbal"]} @ {entryPrice:.2f}/{lastPrice:.2f}') + accountOrders = [order for order in orders # Заявки + if (order['client_code'] == ClientCode or ClientCode == '') and # Выбираем по коду клиента + order['firmid'] == FirmId and # Фирме + order['account'] == TradeAccountId and # Счету + order['flags'] & 0b1 == 0b1] # Активные заявки + for accountOrder in accountOrders: # Пробегаемся по всем заявка + isBuy = accountOrder['flags'] & 0b100 != 0b100 # Заявка на покупку + print(f'- Заявка номер {accountOrder["order_num"]} {"Покупка" if isBuy else "Продажа"} {accountOrder["class_code"]}.{accountOrder["sec_code"]} {accountOrder["qty"]} @ {accountOrder["price"]}') + accountStopOrders = [stopOrder for stopOrder in stopOrders # Стоп заявки + if (stopOrder['client_code'] == ClientCode or ClientCode == '') and # Выбираем по коду клиента + stopOrder['firmid'] == FirmId and # Фирме + stopOrder['account'] == TradeAccountId and # Счету + stopOrder['flags'] & 0b1 == 0b1] # Активные стоп заявки + for accountStopOrder in accountStopOrders: # Пробегаемся по всем стоп заявкам + isBuy = accountStopOrder['flags'] & 0b100 != 0b100 # Заявка на покупку + print(f'- Стоп заявка номер {accountStopOrder["order_num"]} {"Покупка" if isBuy else "Продажа"} {accountStopOrder["class_code"]}.{accountStopOrder["sec_code"]} {accountStopOrder["qty"]} @ {accountStopOrder["price"]}') + +if __name__ == '__main__': # Точка входа при запуске этого скрипта + # qpProvider = qp.QuikPy() # Вызываем конструктор QuikPy с подключением к локальному компьютеру с QUIK + qpProvider = qp.QuikPy(Host='192.168.1.7') # Вызываем конструктор QuikPy с подключением к удаленному компьютеру с QUIK + + GetAllAccounts() # Получаем все счета. По ним можно будет сформировать список счетов для торговли + print() + GetAccount() # Российские фьючерсы и опционы (счет по умолчанию) + + # Выход + qpProvider.CloseConnectionAndThread() # Перед выходом закрываем соединение и поток QuikPy из любого экземпляра diff --git a/Examples/03 - Ticker.py b/Examples/03 - Ticker.py new file mode 100644 index 0000000..f6d5417 --- /dev/null +++ b/Examples/03 - Ticker.py @@ -0,0 +1,38 @@ +from datetime import datetime +from QuikPy import QuikPy as qp # QuikPy = Работа с Quik из Python через LUA скрипты QuikSharp + + +if __name__ == '__main__': # Точка входа при запуске этого скрипта + # qpProvider = qp.QuikPy() # Вызываем конструктор QuikPy с подключением к локальному компьютеру с QUIK + qpProvider = qp.QuikPy(Host='192.168.1.7') # Вызываем конструктор QuikPy с подключением к удаленному компьютеру с QUIK + + firmId = 'MC0063100000' # Фирма + classCode = 'TQBR' # Класс тикера + secCode = 'GAZP' # Тикер + + # firmId = 'SPBFUT' # Фирма + # classCode = 'SPBFUT' # Класс тикера + # secCode = 'SiH1' # Для фьючерсов: <Код тикера><Месяц экспирации: 3-H, 6-M, 9-U, 12-Z><Последняя цифра года> + + # Данные тикера и его торговый счет + securityInfo = qpProvider.GetSecurityInfo(classCode, secCode)["data"] + print(f'Информация о тикере {classCode}.{secCode} ({securityInfo["short_name"]}):') + print(f'Валюта: {securityInfo["face_unit"]}') + print(f'Кол-во десятичных знаков: {securityInfo["scale"]}') + print(f'Лот: {securityInfo["lot_size"]}') + print(f'Шаг цены: {securityInfo["min_price_step"]}') + print(f'Торговый счет для тикера класса {classCode}: {qpProvider.GetTradeAccount(classCode)["data"]}') + + # Свечки + print(f'5-и минутные свечки {classCode}.{secCode}:') + bars = qpProvider.GetCandlesFromDataSource(classCode, secCode, 5, 0)["data"] # 5 минут, 0 = все свечки + print(bars) + + # print(f'Дневные свечки {classCode}.{secCode}:') + # bars = qpProvider.GetCandlesFromDataSource(classCode, secCode, 1440, 0)['data'] # 1440 минут = 1 день, 0 = все свечки + # dtjs = [row['datetime'] for row in bars] # Получаем исходники даты и времени начала свчки (List comprehensions) + # dts = [datetime(dtj['year'], dtj['month'], dtj['day'], dtj['hour'], dtj['min']) for dtj in dtjs] # Получаем дату и время + # print(dts) + + # Выход + qpProvider.CloseConnectionAndThread() # Перед выходом закрываем соединение и поток QuikPy из любого экземпляра diff --git a/Examples/04 - Stream.py b/Examples/04 - Stream.py new file mode 100644 index 0000000..e20ecaf --- /dev/null +++ b/Examples/04 - Stream.py @@ -0,0 +1,51 @@ +import time # Подписка на события по времени +from QuikPy import QuikPy as qp # QuikPy = Работа с Quik из Python через LUA скрипты QuikSharp + + +def PrintCallback(data): + """Пользовательский обработчик событий: + - Изменение стакана котировок + - Получение обезличенной сделки + - Получение новой свечки + """ + print(data['data']) # Печатаем полученные данные + +if __name__ == '__main__': # Точка входа при запуске этого скрипта + # qpProvider = qp.QuikPy() # Вызываем конструктор QuikPy с подключением к локальному компьютеру с QUIK + qpProvider = qp.QuikPy(Host='192.168.1.7') # Вызываем конструктор QuikPy с подключением к удаленному компьютеру с QUIK + + firmId = 'MC0063100000' # Фирма + classCode = 'TQBR' # Класс тикера + secCode = 'GAZP' # Тикер + + # firmId = 'SPBFUT' # Фирма + # classCode = 'SPBFUT' # Класс тикера + # secCode = 'SiH1' # Для фьючерсов: <Код тикера><Месяц экспирации: 3-H, 6-M, 9-U, 12-Z><Последняя цифра года> + + # Стакан + print(f'Текущий стакан {classCode}.{secCode}: {qpProvider.GetQuoteLevel2(classCode, secCode)}') + qpProvider.OnQuote = PrintCallback # Обработчик изменения стакана котировок + print(f'Подписка на стакан {classCode}.{secCode}: {qpProvider.SubscribeLevel2Quotes(classCode, secCode)["data"]}') + sleepSec = 1 # Кол-во секунд получения котировок + print(f'{sleepSec} секунд котировок') + time.sleep(sleepSec) # Ждем кол-во секунд получения котировок + print(f'Отмена подписки на стакан: {qpProvider.UnsubscribeLevel2Quotes(classCode, secCode)["data"]}') + print(f'Статус подписки: {qpProvider.IsSubscribedLevel2Quotes(classCode, secCode)["data"]}') + qpProvider.OnQuote = qpProvider.DefaultHandler # Возвращаем обработчик по умолчанию + + # Обезличенные сделки. Чтобы получать, в QUIK открыть Таблицу обезличенных сделок, указать тикер + qpProvider.OnAllTrade = PrintCallback # Обработчик получения обезличенной сделки + sleepSec = 3 # Кол-во секунд получения обезличенных сделок + print(f'{sleepSec} секунд обезличенных сделок') + time.sleep(sleepSec) # Ждем кол-во секунд получения обезличенных сделок + qpProvider.OnAllTrade = qpProvider.DefaultHandler # Возвращаем обработчик по умолчанию + + # Подписка на новые свечки + qpProvider.OnNewCandle = PrintCallback # Обработчик получения новой свечки - В первый раз получим все свечки с начала прошлой сессии + print(f'Подписка на минутные свечки {qpProvider.SubscribeToCandles(classCode, secCode, 1)["data"]}') + input('Enter - отмена') + print(f'Отмена подписки на минутные свечки {qpProvider.UnsubscribeFromCandles(classCode, secCode, 1)["data"]}') + qpProvider.OnNewCandle = qpProvider.DefaultHandler # Возвращаем обработчик по умолчанию + + # Выход + qpProvider.CloseConnectionAndThread() # Перед выходом закрываем соединение и поток QuikPy из любого экземпляра diff --git a/Examples/05 - Transactions.py b/Examples/05 - Transactions.py new file mode 100644 index 0000000..41525e7 --- /dev/null +++ b/Examples/05 - Transactions.py @@ -0,0 +1,90 @@ +from QuikPy import QuikPy as qp # QuikPy = Работа с Quik из Python через LUA скрипты QuikSharp + + +def OnTransReply(data): + """Обработчик события ответа на транзакцию пользователя""" + print('OnTransReply') + print(data['data']) # Печатаем полученные данные + +def OnOrder(data): + """Обработчик события получения новой / изменения существующей заявки""" + print('OnOrder') + print(data['data']) # Печатаем полученные данные + +def OnTrade(data): + """Обработчик события получения новой / изменения существующей сделки + Не вызывается при закрытии сделки + """ + print('OnTrade') + print(data['data']) # Печатаем полученные данные + +def OnFuturesClientHolding(data): + """Обработчик события изменения позиции по срочному рынку""" + print('OnFuturesClientHolding') + print(data['data']) # Печатаем полученные данные + +def OnDepoLimit(data): + """Обработчик события изменения позиции по инструментам""" + print('OnDepoLimit') + print(data['data']) # Печатаем полученные данные + +def OnDepoLimitDelete(data): + """Обработчик события удаления позиции по инструментам""" + print('OnDepoLimitDelete') + print(data['data']) # Печатаем полученные данные + +if __name__ == '__main__': # Точка входа при запуске этого скрипта + # qpProvider = qp.QuikPy() # Вызываем конструктор QuikPy с подключением к локальному компьютеру с QUIK + qpProvider = qp.QuikPy(Host='192.168.1.7') # Вызываем конструктор QuikPy с подключением к удаленному компьютеру с QUIK + qpProvider.OnTransReply = OnTransReply # Ответ на транзакцию пользователя. Если транзакция выполняется из QUIK, то не вызывается + qpProvider.OnOrder = OnOrder # Получение новой / изменение существующей заявки + qpProvider.OnTrade = OnTrade # Получение новой / изменение существующей сделки + qpProvider.OnFuturesClientHolding = OnFuturesClientHolding # Изменение позиции по срочному рынку + qpProvider.OnDepoLimit = OnDepoLimit # Изменение позиции по инструментам + qpProvider.OnDepoLimitDelete = OnDepoLimitDelete # Удаление позиции по инструментам + + TransId = 12345 # Номер транзакции + price = 74550 # Цена входа/выхода + quantity = 1 # Кол-во в лотах + + # Новая лимитная/рыночная заявка + transaction = { # Все значения должны передаваться в виде строк + 'TRANS_ID': str(TransId), # Номер транзакции задается клиентом + 'CLIENT_CODE': '', # Код клиента. Для фьючерсов его нет + 'ACCOUNT': 'SPBFUT00PST', # Счет + 'ACTION': 'NEW_ORDER', # Тип заявки: Новая лимитная/рыночная заявка + 'CLASSCODE': 'SPBFUT', # Код площадки + 'SECCODE': 'SiH1', # Код тикера + 'OPERATION': 'S', # B = покупка, S = продажа + 'PRICE': str(price), # Цена исполнения. Для рыночных фьючерсных заявок наихудшая цена в зависимости от направления. Для остальных рыночных заявок цена = 0 + 'QUANTITY': str(quantity), # Кол-во в лотах + 'TYPE': 'L'} # L = лимитная заявка (по умолчанию), M = рыночная заявка + print(f'Новая лимитная/рыночная заявка отправлена на рынок: {qpProvider.SendTransaction(transaction)["data"]}') + + # Новая стоп заявка + # transaction = { # Все значения должны передаваться в виде строк + # 'TRANS_ID': str(TransId), # Номер транзакции задается клиентом + # 'CLIENT_CODE': '', # Код клиента. Для фьючерсов его нет + # 'ACCOUNT': 'SPBFUT00PST', # Счет + # 'ACTION': 'NEW_STOP_ORDER', # Тип заявки: Новая стоп заявка + # 'CLASSCODE': 'SPBFUT', # Код площадки + # 'SECCODE': 'SiH1', # Код тикера + # 'OPERATION': 'B', # B = покупка, S = продажа + # 'PRICE': str(price), # Цена исполнения + # 'QUANTITY': str(quantity), # Кол-во в лотах + # 'STOPPRICE': str(price), # Стоп цена исполнения + # 'EXPIRY_DATE': 'GTC'} # Срок действия до отмены + # print(f'Новая стоп заявка отправлена на рынок: {qpProvider.SendTransaction(transaction)["data"]}') + + # Удаление существующей заявки + # orderNum = 1234567890123456789 # 19-и значный номер заявки + # transaction = { + # 'TRANS_ID': str(TransId), # Номер транзакции задается клиентом + # 'ACTION': 'KILL_ORDER', # Тип заявки: Удаление существующей заявки + # 'CLASSCODE': 'SPBFUT', # Код площадки + # 'SECCODE': 'SiH1', # Код тикера + # 'ORDER_KEY': str(orderNum)} # Номер заявки + # print(f'Удаление заявки отправлено на рынок: {qpProvider.SendTransaction(transaction)["data"]}') + + input('Enter - отмена') # Ждем исполнение заявки + qpProvider.CloseConnectionAndThread() # Перед выходом закрываем соединение и поток QuikPy из любого экземпляра diff --git a/QUIK/lua/QuikSharp.lua b/QUIK/lua/QuikSharp.lua new file mode 100644 index 0000000..69a5146 --- /dev/null +++ b/QUIK/lua/QuikSharp.lua @@ -0,0 +1,140 @@ +--~ Copyright (c) 2014-2020 QUIKSharp Authors https://github.com/finsight/QUIKSharp/blob/master/AUTHORS.md. All rights reserved. +--~ Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information. + +-- is running from Quik +function is_quik() + if getScriptPath then return true else return false end +end + +quikVersion = nil + +script_path = "." + +if is_quik() then + script_path = getScriptPath() + + quikVersion = getInfoParam("VERSION") + + if quikVersion ~= nil then + local t={} + for str in string.gmatch(quikVersion, "([^%.]+)") do + table.insert(t, str) + end + quikVersion = tonumber(t[1]) * 100 + tonumber(t[2]) + end + + if quikVersion == nil then + message("QUIK# cannot detect QUIK version", 3) + return + else + libPath = "\\clibs" + end + + -- MD dynamic, requires MSVCRT + -- MT static, MSVCRT is linked statically with luasocket + -- package.cpath contains info.exe working directory, which has MSVCRT, so MT should not be needed in theory, + -- but in one issue someone said it doesn't work on machines that do not have Visual Studio. + local linkage = "MD" + + if quikVersion >= 805 then + libPath = libPath .. "64\\53_"..linkage.."\\" + elseif quikVersion >= 800 then + libPath = libPath .. "64\\5.1_"..linkage.."\\" + else + libPath = "\\clibs\\5.1_"..linkage.."\\" + end +end +package.path = package.path .. ";" .. script_path .. "\\?.lua;" .. script_path .. "\\?.luac"..";"..".\\?.lua;"..".\\?.luac" +package.cpath = package.cpath .. ";" .. script_path .. libPath .. '?.dll'..";".. '.' .. libPath .. '?.dll' + +local util = require("qsutils") +local qf = require("qsfunctions") +require("qscallbacks") + +log("Detected Quik version: ".. quikVersion .." and using cpath: "..package.cpath , 0) + +local is_started = true + +-- we need two ports since callbacks and responses conflict and write to the same socket at the same time +-- I do not know how to make locking in Lua, it is just simpler to have two independent connections +-- To connect to a remote terminal - replace '127.0.0.1' with the terminal ip-address +-- All this values could be replaced with values from config.json +local response_host = '127.0.0.1' +local response_port = 34130 +local callback_host = '127.0.0.1' +local callback_port = response_port + 1 + +function do_main() + log("Entered main function", 0) + while is_started do + -- if not connected, connect + util.connect(response_host, response_port, callback_host, callback_port) + -- when connected, process queue + -- receive message, + local requestMsg = receiveRequest() + if requestMsg then + -- if ok, process message + -- dispatch_and_process never throws, it returns lua errors wrapped as a message + local responseMsg, err = qf.dispatch_and_process(requestMsg) + if responseMsg then + -- send message + local res = sendResponse(responseMsg) + else + log("Could not dispatch and process request: " .. err, 3) + end + else + delay(1) + end + end +end + +function main() + setup("QuikSharp") + run() +end + +--- catch errors +function run() + local status, err = pcall(do_main) + if status then + log("finished") + else + log(err, 3) + end +end + +function setup(script_name) + if not script_name then + log("File name of this script is unknown. Please, set it explicity instead of scriptFilename() call inside your custom file", 3) + return false + end + + local list = paramsFromConfig(script_name) + if list then + response_host = list[1] + response_port = list[2] + callback_host = list[3] + callback_port = list[4] + printRunningMessage(script_name) + elseif script_name == "QuikSharp" then + -- use default values for this file in case no custom config found for it + printRunningMessage(script_name) + else -- do nothing when config is not found + log("File config.json is not found or contains no entries for this script name: " .. script_name, 3) + return false + end + + return true +end + +function printRunningMessage(script_name) + log("Running from ".. script_name .. ", params: response " .. response_host .. ":" .. response_port ..", callback ".. " ".. callback_host ..":".. callback_port) +end + +if not is_quik() then + log("Hello, QUIK#! Running outside Quik.") + setup("QuikSharp") + do_main() + logfile:close() +end + diff --git a/QUIK/lua/Quik_2.lua b/QUIK/lua/Quik_2.lua new file mode 100644 index 0000000..006792c --- /dev/null +++ b/QUIK/lua/Quik_2.lua @@ -0,0 +1,15 @@ +--~ Copyright (c) 2014-2020 QUIKSharp Authors https://github.com/finsight/QUIKSharp/blob/master/AUTHORS.md. All rights reserved. +--~ Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information. + +script_path = getScriptPath() +package.path = package.path .. ";" .. script_path .. "\\?.lua;" .. script_path .. "\\?.luac"..";"..".\\?.lua;"..".\\?.luac" +require("QuikSharp") + +-- Do not edit this file. Just copy it and save with a different name. Then write required params for it inside config.json file +-- Не редактируйте этой файл. Просто скопируйте и сохраните под другим именем. После этого укажите настройки для него в файле config.json + +function main() + if setup(scriptFilename()) then + run() + end +end \ No newline at end of file diff --git a/QUIK/lua/config.json b/QUIK/lua/config.json new file mode 100644 index 0000000..76d5905 --- /dev/null +++ b/QUIK/lua/config.json @@ -0,0 +1,18 @@ +{ + "servers": [ + { + "scriptName": "QuikSharp", + "responseHostname": "*", + "responsePort": 34130, + "callbackHostname": "*", + "callbackPort": 34131 + }, + { + "scriptName": "Quik_2", + "responseHostname": "*", + "responsePort": 34132, + "callbackHostname": "*", + "callbackPort": 34133 + } + ] +} \ No newline at end of file diff --git a/QUIK/lua/dkjson.lua b/QUIK/lua/dkjson.lua new file mode 100644 index 0000000..fa50b9f --- /dev/null +++ b/QUIK/lua/dkjson.lua @@ -0,0 +1,714 @@ +-- Module options: +local always_try_using_lpeg = true +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1/5.2 + +Version 2.5 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2013 David Heiko Kolf + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset = + pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.5" } + +if register_global_module_table then + _G[global_module_name] = json +end + +local _ENV = nil -- blocking globals in Lua 5.2 + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return "line " .. line .. ", column " .. (where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local len = strlen (str) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +function json.use_lpeg () + local g = require ("lpeg") + + if g.version() == "0.11" then + error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" + end + + local pegmatch = g.match + local P, S, R = g.P, g.S, g.R + + local function ErrorCall (str, pos, msg, state) + if not state.msg then + state.msg = msg .. " at " .. loc (str, pos) + state.pos = pos + end + return false + end + + local function Err (msg) + return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) + end + + local SingleLineComment = P"//" * (1 - S"\n\r")^0 + local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" + local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + + local PlainChar = 1 - S"\"\\\n\r" + local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars + local HexDigit = R("09", "af", "AF") + local function UTF16Surrogate (match, pos, high, low) + high, low = tonumber (high, 16), tonumber (low, 16) + if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then + return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) + else + return false + end + end + local function UTF16BMP (hex) + return unichar (tonumber (hex, 16)) + end + local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) + local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP + local Char = UnicodeEscape + EscapeSequence + PlainChar + local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string") + local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) + local Fractal = P"." * R"09"^0 + local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 + local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num + local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) + local SimpleValue = Number + String + Constant + local ArrayContent, ObjectContent + + -- The functions parsearray and parseobject parse only a single value/pair + -- at a time and store them directly to avoid hitting the LPeg limits. + local function parsearray (str, pos, nullval, state) + local obj, cont + local npos + local t, nt = {}, 0 + repeat + obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) + if not npos then break end + pos = npos + nt = nt + 1 + t[nt] = obj + until cont == 'last' + return pos, setmetatable (t, state.arraymeta) + end + + local function parseobject (str, pos, nullval, state) + local obj, key, cont + local npos + local t = {} + repeat + key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) + if not npos then break end + pos = npos + t[key] = obj + until cont == 'last' + return pos, setmetatable (t, state.objectmeta) + end + + local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected") + local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected") + local Value = Space * (Array + Object + SimpleValue) + local ExpectedValue = Value + Space * Err "value expected" + ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() + local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue) + ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() + local DecodeValue = ExpectedValue * g.Cp () + + function json.decode (str, pos, nullval, ...) + local state = {} + state.objectmeta, state.arraymeta = optionalmetatables(...) + local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) + if state.msg then + return nil, state.pos, state.msg + else + return obj, retpos + end + end + + -- use this function only once: + json.use_lpeg = function () return json end + + json.using_lpeg = true + + return json -- so you can get the module using json = require "dkjson".use_lpeg() +end + +if always_try_using_lpeg then + pcall (json.use_lpeg) +end + +return json + diff --git a/QUIK/lua/qscallbacks.lua b/QUIK/lua/qscallbacks.lua new file mode 100644 index 0000000..8a3a6b8 --- /dev/null +++ b/QUIK/lua/qscallbacks.lua @@ -0,0 +1,235 @@ +--~ Copyright (c) 2014-2020 QUIKSharp Authors https://github.com/finsight/QUIKSharp/blob/master/AUTHORS.md. All rights reserved. +--~ Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information. + +package.path = package.path..";"..".\\?.lua;"..".\\?.luac" + +local qscallbacks = {} + +local function CleanUp() + closeLog() +end + +function OnQuikSharpDisconnected() + -- TODO any recovery or risk management logic here +end + +function OnError(message) + if is_connected then + local msg = {} + msg.t = timemsec() + msg.cmd = "lua_error" + msg.data = "Lua error: " .. message + sendCallback(msg) + end +end + +function OnDisconnected() + local msg = {} + msg.cmd = "OnDisconnected" + msg.t = timemsec() + msg.data = "" + sendCallback(msg) +end + +function OnConnected() + local msg = {} + msg.cmd = "OnConnected" + msg.t = timemsec() + msg.data = "" + sendCallback(msg) +end + +function OnAllTrade(alltrade) + if is_connected then + local msg = {} + msg.t = timemsec() + msg.cmd = "OnAllTrade" + msg.data = alltrade + sendCallback(msg) + end +end + +function OnClose() + if is_connected then + local msg = {} + msg.cmd = "OnClose" + msg.t = timemsec() + msg.data = "" + sendCallback(msg) + end + CleanUp() +end + +function OnInit(script_path) + if is_connected then + local msg = {} + msg.cmd = "OnInit" + msg.t = timemsec() + msg.data = script_path + sendCallback(msg) + end + log("QUIK# is initialized from "..script_path, 0) +end + +function OnOrder(order) + local msg = {} + msg.t = timemsec() + msg.id = nil + msg.data = order + msg.cmd = "OnOrder" + sendCallback(msg) +end + +function OnQuote(class_code, sec_code) + if is_connected then + local msg = {} + msg.cmd = "OnQuote" + msg.t = timemsec() + local server_time = getInfoParam("SERVERTIME") + local status, ql2 = pcall(getQuoteLevel2, class_code, sec_code) + if status then + msg.data = ql2 + msg.data.class_code = class_code + msg.data.sec_code = sec_code + msg.data.server_time = server_time + sendCallback(msg) + else + OnError(ql2) + end + end +end + +function OnStop(s) + is_started = false + + if is_connected then + local msg = {} + msg.cmd = "OnStop" + msg.t = timemsec() + msg.data = s + sendCallback(msg) + end + log("QUIK# stopped. You could keep script running when closing QUIK and the script will start automatically the next time you start QUIK", 1) + CleanUp() + -- send disconnect + return 1000 +end + +function OnTrade(trade) + local msg = {} + msg.t = timemsec() + msg.id = nil + msg.data = trade + msg.cmd = "OnTrade" + sendCallback(msg) +end + +function OnTransReply(trans_reply) + local msg = {} + msg.t = timemsec() + msg.id = nil + msg.data = trans_reply + msg.cmd = "OnTransReply" + sendCallback(msg) +end + +function OnStopOrder(stop_order) + local msg = {} + msg.t = timemsec() + msg.data = stop_order + msg.cmd = "OnStopOrder" + sendCallback(msg) +end + +function OnParam(class_code, sec_code) + local msg = {} + msg.cmd = "OnParam" + msg.t = timemsec() + local dat = {} + dat.class_code = class_code + dat.sec_code = sec_code + msg.data = dat + sendCallback(msg) +end + +function OnAccountBalance(acc_bal) + local msg = {} + msg.t = timemsec() + msg.data = acc_bal + msg.cmd = "OnAccountBalance" + sendCallback(msg) +end + +function OnAccountPosition(acc_pos) + local msg = {} + msg.t = timemsec() + msg.data = acc_pos + msg.cmd = "OnAccountPosition" + sendCallback(msg) +end + +function OnDepoLimit(dlimit) + local msg = {} + msg.t = timemsec() + msg.data = dlimit + msg.cmd = "OnDepoLimit" + sendCallback(msg) +end + +function OnDepoLimitDelete(dlimit_del) + local msg = {} + msg.t = timemsec() + msg.data = dlimit_del + msg.cmd = "OnDepoLimitDelete" + sendCallback(msg) +end + +function OnFirm(firm) + local msg = {} + msg.t = timemsec() + msg.data = firm + msg.cmd = "OnFirm" + sendCallback(msg) +end + +function OnFuturesClientHolding(fut_pos) + local msg = {} + msg.t = timemsec() + msg.data = fut_pos + msg.cmd = "OnFuturesClientHolding" + sendCallback(msg) +end + +function OnFuturesLimitChange(fut_limit) + local msg = {} + msg.t = timemsec() + msg.data = fut_limit + msg.cmd = "OnFuturesLimitChange" + sendCallback(msg) +end + +function OnFuturesLimitDelete(lim_del) + local msg = {} + msg.t = timemsec() + msg.data = lim_del + msg.cmd = "OnFuturesLimitDelete" + sendCallback(msg) +end + +function OnMoneyLimit(mlimit) + local msg = {} + msg.t = timemsec() + msg.data = mlimit + msg.cmd = "OnMoneyLimit" + sendCallback(msg) +end + +function OnMoneyLimitDelete(mlimit_del) + local msg = {} + msg.t = timemsec() + msg.data = mlimit_del + msg.cmd = "OnMoneyLimitDelete" + sendCallback(msg) +end + +return qscallbacks diff --git a/QUIK/lua/qsfunctions.lua b/QUIK/lua/qsfunctions.lua new file mode 100644 index 0000000..9d3faf8 --- /dev/null +++ b/QUIK/lua/qsfunctions.lua @@ -0,0 +1,940 @@ +--~ Copyright (c) 2014-2020 QUIKSharp Authors https://github.com/finsight/QUIKSharp/blob/master/AUTHORS.md. All rights reserved. +--~ Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information. + +local json = require ("dkjson") +local qsfunctions = {} + +function qsfunctions.dispatch_and_process(msg) + if qsfunctions[msg.cmd] then + -- dispatch a command simply by a table lookup + -- in qsfunctions method names must match commands + local status, result = pcall(qsfunctions[msg.cmd], msg) + if status then + return result + else + msg.cmd = "lua_error" + msg.lua_error = "Lua error: " .. result + return msg + end + else + log(to_json(msg), 3) + msg.lua_error = "Command not implemented in Lua qsfunctions module: " .. msg.cmd + msg.cmd = "lua_error" + return msg + end +end + +--------------------- +-- Debug functions -- +--------------------- + +--- Returns Pong to Ping +-- @param msg message table +-- @return same msg table +function qsfunctions.ping(msg) + -- need to know data structure the caller gives + msg.t = 0 -- avoid time generation. Could also leave original + if msg.data == "Ping" then + msg.data = "Pong" + return msg + else + msg.data = msg.data .. " is not Ping" + return msg + end +end + +--- Echoes its message +function qsfunctions.echo(msg) + return msg +end + +--- Test error handling +function qsfunctions.divide_string_by_zero(msg) + --noinspection LuaDivideByZero + msg.data = "asd" / 0 + return msg +end + +--- Is running inside quik +function qsfunctions.is_quik(msg) + if getScriptPath then msg.data = 1 else msg.data = 0 end + return msg +end + +----------------------- +-- Service functions -- +----------------------- + +--- Функция предназначена для определения состояния подключения клиентского места к +-- серверу. Возвращает «1», если клиентское место подключено и «0», если не подключено. +function qsfunctions.isConnected(msg) + -- set time when function was called + msg.t = timemsec() + msg.data = isConnected() + return msg +end + +--- Функция возвращает путь, по которому находится файл info.exe, исполняющий данный +-- скрипт, без завершающего обратного слэша («\»). Например, C:\QuikFront. +function qsfunctions.getWorkingFolder(msg) + -- set time when function was called + msg.t = timemsec() + msg.data = getWorkingFolder() + return msg +end + +--- Функция возвращает путь, по которому находится запускаемый скрипт, без завершающего +-- обратного слэша («\»). Например, C:\QuikFront\Scripts. +function qsfunctions.getScriptPath(msg) + -- set time when function was called + msg.t = timemsec() + msg.data = getScriptPath() + return msg +end + +--- Функция возвращает значения параметров информационного окна (пункт меню +-- Связь / Информационное окно…). +function qsfunctions.getInfoParam(msg) + -- set time when function was called + msg.t = timemsec() + msg.data = getInfoParam(msg.data) + return msg +end + +--- Функция отображает сообщения в терминале QUIK. +function qsfunctions.message(msg) + log(msg.data, 1) + msg.data = "" + return msg +end +function qsfunctions.warning_message(msg) + log(msg.data, 2) + msg.data = "" + return msg +end +function qsfunctions.error_message(msg) + log(msg.data, 3) + msg.data = "" + return msg +end + +--- Функция приостанавливает выполнение скрипта. +function qsfunctions.sleep(msg) + delay(msg.data) + msg.data = "" + return msg +end + +--- Функция для вывода отладочной информации. +function qsfunctions.PrintDbgStr(msg) + log(msg.data, 0) + msg.data = "" + return msg +end + +-- Выводит на график метку +function qsfunctions.addLabel(msg) + local spl = split(msg.data, "|") + local price, curdate, curtime, qty, path, id, algmnt, bgnd = spl[1], spl[2], spl[3], spl[4], spl[5], spl[6], spl[7], spl[8] + local label = { + TEXT = "", + IMAGE_PATH = path, + ALIGNMENT = algmnt, + YVALUE = tostring(price), + DATE = tostring(curdate), + TIME = tostring(curtime), + R = 255, + G = 255, + B = 255, + TRANSPARENCY = 0, + TRANSPARENT_BACKGROUND = bgnd, + FONT_FACE_NAME = "Arial", + FONT_HEIGHT = "15", + HINT = " " .. tostring(price) .. " " .. tostring(qty) + } + local res = AddLabel(id, label) + msg.data = res + return msg +end + +-- Удаляем выбранную метку +function qsfunctions.delLabel(msg) + local spl = split(msg.data, "|") + local tag, id = spl[1], spl[2] + DelLabel(tag, tonumber(id)) + msg.data = "" + return msg +end + +-- Удаляем все метки с графика +function qsfunctions.delAllLabels(msg) + local spl = split(msg.data, "|") + local id = spl[1] + DelAllLabels(id) + msg.data = "" + return msg +end + +--------------------- +-- Class functions -- +--------------------- + +--- Функция предназначена для получения списка кодов классов, переданных с сервера в ходе сеанса связи. +function qsfunctions.getClassesList(msg) + msg.data = getClassesList() +-- if msg.data then log(msg.data) else log("getClassesList returned nil") end + return msg +end + +--- Функция предназначена для получения информации о классе. +function qsfunctions.getClassInfo(msg) + msg.data = getClassInfo(msg.data) +-- if msg.data then log(msg.data.name) else log("getClassInfo returned nil") end + return msg +end + +--- Функция предназначена для получения списка кодов бумаг для списка классов, заданного списком кодов. +function qsfunctions.getClassSecurities(msg) + msg.data = getClassSecurities(msg.data) +-- if msg.data then log(msg.data) else log("getClassSecurities returned nil") end + return msg +end + +--- Функция получает информацию по указанному классу и бумаге. +function qsfunctions.getSecurityInfo(msg) + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + msg.data = getSecurityInfo(class_code, sec_code) + return msg +end + +--- Функция берет на вход список из элементов в формате class_code|sec_code и возвращает список ответов функции getSecurityInfo. +-- Если какая-то из бумаг не будет найдена, вместо ее значения придет null +function qsfunctions.getSecurityInfoBulk(msg) + local result = {} + for i=1,#msg.data do + local spl = split(msg.data[i], "|") + local class_code, sec_code = spl[1], spl[2] + + local status, security = pcall(getSecurityInfo, class_code, sec_code) + if status and security then + table.insert(result, security) + else + if not status then + log("Error happened while calling getSecurityInfoBulk with ".. class_code .. "|".. sec_code .. ": ".. security) + end + table.insert(result, json.null) + end + end + msg.data = result + return msg +end + +--- Функция предназначена для определения класса по коду инструмента из заданного списка классов. +function qsfunctions.getSecurityClass(msg) + local spl = split(msg.data, "|") + local classes_list, sec_code = spl[1], spl[2] + + for class_code in string.gmatch(classes_list,"([^,]+)") do + if getSecurityInfo(class_code,sec_code) then + msg.data = class_code + return msg + end + end + msg.data = "" + return msg +end + +--- Функция возвращает код клиента +function qsfunctions.getClientCode(msg) + for i=0,getNumberOf("MONEY_LIMITS")-1 do + local clientcode = getItem("MONEY_LIMITS",i).client_code + if clientcode ~= nil then + msg.data = clientcode + return msg + end + end + return msg +end + +--- Функция возвращает все коды клиента +function qsfunctions.getClientCodes(msg) + local client_codes = {} + for i=0,getNumberOf("MONEY_LIMITS")-1 do + local clientcode = getItem("MONEY_LIMITS",i).client_code + if clientcode ~= nil then + table.insert(client_codes, clientcode) + end + end + msg.data = client_codes + return msg +end + +--- Функция возвращает торговый счет для запрашиваемого кода класса +function qsfunctions.getTradeAccount(msg) + for i=0,getNumberOf("trade_accounts")-1 do + local trade_account = getItem("trade_accounts",i) + if string.find(trade_account.class_codes,'|' .. msg.data .. '|',1,1) then + msg.data = trade_account.trdaccid + return msg + end + end + return msg +end + +--- Функция возвращает торговые счета в системе, у которых указаны поддерживаемые классы инструментов. +function qsfunctions.getTradeAccounts(msg) + local trade_accounts = {} + for i=0,getNumberOf("trade_accounts")-1 do + local trade_account = getItem("trade_accounts",i) + if trade_account.class_codes ~= "" then + table.insert(trade_accounts, trade_account) + end + end + msg.data = trade_accounts + return msg +end + + + +--------------------------------------------------------------------- +-- Order Book functions (Функции для работы СЃРѕ стаканом котировок) -- +--------------------------------------------------------------------- + +--- Функция заказывает на сервер получение стакана по указанному классу и бумаге. +function qsfunctions.Subscribe_Level_II_Quotes(msg) + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + msg.data = Subscribe_Level_II_Quotes(class_code, sec_code) + return msg +end + +--- Функция отменяет заказ на получение с сервера стакана по указанному классу и бумаге. +function qsfunctions.Unsubscribe_Level_II_Quotes(msg) + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + msg.data = Unsubscribe_Level_II_Quotes(class_code, sec_code) + return msg +end + +--- Функция позволяет узнать, заказан ли с сервера стакан по указанному классу и бумаге. +function qsfunctions.IsSubscribed_Level_II_Quotes(msg) + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + msg.data = IsSubscribed_Level_II_Quotes(class_code, sec_code) + return msg +end + +--- Функция предназначена для получения стакана по указанному классу и инструменту. +function qsfunctions.GetQuoteLevel2(msg) + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + local server_time = getInfoParam("SERVERTIME") + local status, ql2 = pcall(getQuoteLevel2, class_code, sec_code) + if status then + msg.data = ql2 + msg.data.class_code = class_code + msg.data.sec_code = sec_code + msg.data.server_time = server_time + else + OnError(ql2) + end + return msg +end + +----------------------- +-- Trading functions -- +----------------------- + +--- отправляет транзакцию на сервер и возвращает пустое сообщение, которое +-- будет проигноировано. Вместо него, отправитель будет ждать события +-- OnTransReply, из которого по TRANS_ID он получит результат отправленной транзакции +function qsfunctions.sendTransaction(msg) + local res = sendTransaction(msg.data) + if res~="" then + -- error handling + msg.cmd = "lua_transaction_error" + msg.lua_error = res + return msg + else + -- transaction sent + msg.data = true + return msg + end +end + +--- Функция заказывает получение параметров Таблицы текущих торгов. В случае успешного завершения функция возвращает «true», иначе – «false» +function qsfunctions.paramRequest(msg) + local spl = split(msg.data, "|") + local class_code, sec_code, param_name = spl[1], spl[2], spl[3] + msg.data = ParamRequest(class_code, sec_code, param_name) + return msg +end + +--- Функция принимает список строк (JSON Array) в формате class_code|sec_code|param_name, вызывает функцию paramRequest для каждой строки. +-- Возвращает список ответов в том же порядке +function qsfunctions.paramRequestBulk(msg) + local result = {} + for i=1,#msg.data do + local spl = split(msg.data[i], "|") + local class_code, sec_code, param_name = spl[1], spl[2], spl[3] + table.insert(result, ParamRequest(class_code, sec_code, param_name)) + end + msg.data = result + return msg +end + +--- Функция отменяет заказ на получение параметров Таблицы текущих торгов. В случае успешного завершения функция возвращает «true», иначе – «false» +function qsfunctions.cancelParamRequest(msg) + local spl = split(msg.data, "|") + local class_code, sec_code, param_name = spl[1], spl[2], spl[3] + msg.data = CancelParamRequest(class_code, sec_code, param_name) + return msg +end + +--- Функция принимает список строк (JSON Array) в формате class_code|sec_code|param_name, вызывает функцию CancelParamRequest для каждой строки. +-- Возвращает список ответов в том же порядке +function qsfunctions.cancelParamRequestBulk(msg) + local result = {} + for i=1,#msg.data do + local spl = split(msg.data[i], "|") + local class_code, sec_code, param_name = spl[1], spl[2], spl[3] + table.insert(result, CancelParamRequest(class_code, sec_code, param_name)) + end + msg.data = result + return msg +end + +--- Функция предназначена для получения значений всех параметров биржевой информации из Таблицы текущих значений параметров. +-- С помощью этой функции можно получить любое из значений Таблицы текущих значений параметров для заданных кодов класса и бумаги. +function qsfunctions.getParamEx(msg) + local spl = split(msg.data, "|") + local class_code, sec_code, param_name = spl[1], spl[2], spl[3] + msg.data = getParamEx(class_code, sec_code, param_name) + return msg +end + +--- Функция предназначена для получения значении? всех параметров биржевои? информации из Таблицы текущих торгов +-- с возможностью в дальнеи?шем отказаться от получения определенных параметров, заказанных с помощью функции ParamRequest. +-- Для отказа от получения какого-либо параметра воспользуи?тесь функциеи? CancelParamRequest. +-- Функция возвращает таблицу Lua с параметрами, аналогичными параметрам, возвращаемым функциеи? getParamEx +function qsfunctions.getParamEx2(msg) + local spl = split(msg.data, "|") + local class_code, sec_code, param_name = spl[1], spl[2], spl[3] + msg.data = getParamEx2(class_code, sec_code, param_name) + return msg +end + +--- Функция принимает список строк (JSON Array) в формате class_code|sec_code|param_name и возвращает результаты вызова +-- функции getParamEx2 для каждой строки запроса в виде списка в таком же порядке, как в запросе +function qsfunctions.getParamEx2Bulk(msg) + local result = {} + for i=1,#msg.data do + local spl = split(msg.data[i], "|") + local class_code, sec_code, param_name = spl[1], spl[2], spl[3] + table.insert(result, getParamEx2(class_code, sec_code, param_name)) + end + msg.data = result + return msg +end + +-- Функция предназначена для получения информации по бумажным лимитам. +function qsfunctions.getDepo(msg) + local spl = split(msg.data, "|") + local clientCode, firmId, secCode, account = spl[1], spl[2], spl[3], spl[4] + msg.data = getDepo(clientCode, firmId, secCode, account) + return msg +end + +-- Функция предназначена для получения информации по бумажным лимитам. +function qsfunctions.getDepoEx(msg) + local spl = split(msg.data, "|") + local firmId, clientCode, secCode, account, limit_kind = spl[1], spl[2], spl[3], spl[4], spl[5] + msg.data = getDepoEx(firmId, clientCode, secCode, account, tonumber(limit_kind)) + return msg +end + +-- Функция для получения информации по денежным лимитам. +function qsfunctions.getMoney(msg) + local spl = split(msg.data, "|") + local client_code, firm_id, tag, curr_code = spl[1], spl[2], spl[3], spl[4] + msg.data = getMoney(client_code, firm_id, tag, curr_code) + return msg +end + +-- Функция для получения информации по денежным лимитам указанного типа. +function qsfunctions.getMoneyEx(msg) + local spl = split(msg.data, "|") + local firm_id, client_code, tag, curr_code, limit_kind = spl[1], spl[2], spl[3], spl[4], spl[5] + msg.data = getMoneyEx(firm_id, client_code, tag, curr_code, tonumber(limit_kind)) + return msg +end + +-- Функция возвращает информацию по всем денежным лимитам. +function qsfunctions.getMoneyLimits(msg) + local limits = {} + for i=0,getNumberOf("money_limits")-1 do + local limit = getItem("money_limits",i) + table.insert(limits, limit) + end + msg.data = limits + return msg +end + +-- Функция предназначена для получения информации по фьючерсным лимитам. +function qsfunctions.getFuturesLimit(msg) + local spl = split(msg.data, "|") + local firmId, accId, limitType, currCode = spl[1], spl[2], spl[3], spl[4] + local result, err = getFuturesLimit(firmId, accId, limitType*1, currCode) + if result then + msg.data = result + else + log("Futures limit returns nil", 3) + msg.data = nil + end + return msg +end + +-- Функция возвращает информацию по фьючерсным лимитам для всех торговых счетов. +function qsfunctions.getFuturesClientLimits(msg) + local limits = {} + for i=0,getNumberOf("futures_client_limits")-1 do + local limit = getItem("futures_client_limits",i) + table.insert(limits, limit) + end + msg.data = limits + return msg +end + +--- (ichechet) Через getFuturesHolding позиции не приходили. Пришлось сделать обработку таблицы futures_client_holding +function qsfunctions.getFuturesHolding(msg) + if msg.data ~= "" then + local spl = split(msg.data, "|") + local firmId, accId, secCode, posType = spl[1], spl[2], spl[3], spl[4] + end + + local fchs = {} + for i = 0, getNumberOf("futures_client_holding") - 1 do + local fch = getItem("futures_client_holding", i) + if msg.data == "" or (fch.firmid == firmId and fch.trdaccid == accId and fch.sec_code == secCode and fch.type == posType*1) then + table.insert(fchs, fch) + end + end + msg.data = fchs + return msg +end + +-- Функция возвращает таблицу заявок (всю или по заданному инструменту) +function qsfunctions.get_orders(msg) + if msg.data ~= "" then + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + end + + local orders = {} + for i = 0, getNumberOf("orders") - 1 do + local order = getItem("orders", i) + if msg.data == "" or (order.class_code == class_code and order.sec_code == sec_code) then + table.insert(orders, order) + end + end + msg.data = orders + return msg +end + +-- Функция возвращает заявку по заданному инструменту и ID-транзакции +function qsfunctions.getOrder_by_ID(msg) + if msg.data ~= "" then + local spl = split(msg.data, "|") + local class_code, sec_code, trans_id = spl[1], spl[2], spl[3] + end + + local order_num = 0 + local res + for i = 0, getNumberOf("orders") - 1 do + local order = getItem("orders", i) + if order.class_code == class_code and order.sec_code == sec_code and order.trans_id == tonumber(trans_id) and order.order_num > order_num then + order_num = order.order_num + res = order + end + end + msg.data = res + return msg +end + +---- Функция возвращает заявку по номеру +function qsfunctions.getOrder_by_Number(msg) + for i=0,getNumberOf("orders")-1 do + local order = getItem("orders",i) + if order.order_num == tonumber(msg.data) then + msg.data = order + return msg + end + end + return msg +end + +--- Возвращает заявку по её номеру и классу инструмента --- +--- На основе http://help.qlua.org/ch4_5_1_1.htm --- +function qsfunctions.get_order_by_number(msg) + local spl = split(msg.data, "|") + local class_code = spl[1] + local order_id = tonumber(spl[2]) + msg.data = getOrderByNumber(class_code, order_id) + return msg +end + +--- Возвращает список записей из таблицы 'Лимиты по бумагам' +--- На основе http://help.qlua.org/ch4_6_11.htm и http://help.qlua.org/ch4_5_3.htm +function qsfunctions.get_depo_limits(msg) + local sec_code = msg.data + local count = getNumberOf("depo_limits") + local depo_limits = {} + for i = 0, count - 1 do + local depo_limit = getItem("depo_limits", i) + if msg.data == "" or depo_limit.sec_code == sec_code then + table.insert(depo_limits, depo_limit) + end + end + msg.data = depo_limits + return msg +end + +-- Функция возвращает таблицу сделок (всю или по заданному инструменту) +function qsfunctions.get_trades(msg) + if msg.data ~= "" then + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + end + + local trades = {} + for i = 0, getNumberOf("trades") - 1 do + local trade = getItem("trades", i) + if msg.data == "" or (trade.class_code == class_code and trade.sec_code == sec_code) then + table.insert(trades, trade) + end + end + msg.data = trades + return msg +end + +-- Функция возвращает таблицу сделок по номеру заявки +function qsfunctions.get_Trades_by_OrderNumber(msg) + local order_num = tonumber(msg.data) + + local trades = {} + for i = 0, getNumberOf("trades") - 1 do + local trade = getItem("trades", i) + if trade.order_num == order_num then + table.insert(trades, trade) + end + end + msg.data = trades + return msg +end + +-- Функция предназначена для получения значений параметров таблицы «Клиентский портфель», соответствующих идентификатору участника торгов «firmid» и коду клиента «client_code». +function qsfunctions.getPortfolioInfo(msg) + local spl = split(msg.data, "|") + local firmId, clientCode = spl[1], spl[2] + msg.data = getPortfolioInfo(firmId, clientCode) + return msg +end + +-- Функция предназначена для получения значений параметров таблицы «Клиентский портфель», соответствующих идентификатору участника торгов «firmid», коду клиента «client_code» и виду лимита «limit_kind». +function qsfunctions.getPortfolioInfoEx(msg) + local spl = split(msg.data, "|") + local firmId, clientCode, limit_kind = spl[1], spl[2], spl[3] + msg.data = getPortfolioInfoEx(firmId, clientCode, tonumber(limit_kind)) + return msg +end + +-- Функция предназначена для получения таблицы обезличенных сделок по выбранному инструменту или всю целиком. +function qsfunctions.get_all_trades(msg) + if msg.data ~= "" then + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + end + + local trades = {} + for i = 0, getNumberOf("all_trades") - 1 do + local trade = getItem("all_trades", i) + if msg.data == "" or (trade.class_code == class_code and trade.sec_code == sec_code) then + table.insert(trades, trade) + end + end + msg.data = trades + return msg +end + + +-------------------------- +-- OptionBoard functions -- +-------------------------- +function qsfunctions.getOptionBoard(msg) + local spl = split(msg.data, "|") + local classCode, secCode = spl[1], spl[2] + local result, err = getOptions(classCode, secCode) + if result then + msg.data = result + else + log("Option board returns nil", 3) + msg.data = nil + end + return msg +end + +function getOptions(classCode,secCode) + --classCode = "SPBOPT" +--BaseSecList="RIZ6" +local SecList = getClassSecurities(classCode) --все сразу +local t={} +local p={} +for sec in string.gmatch(SecList, "([^,]+)") do --перебираем опционы по очереди. + local Optionbase=getParamEx(classCode,sec,"optionbase").param_image + local Optiontype=getParamEx(classCode,sec,"optiontype").param_image + if (string.find(secCode,Optionbase)~=nil) then + + + p={ + ["code"]=getParamEx(classCode,sec,"code").param_image, + ["Name"]=getSecurityInfo(classCode,sec).name, + ["DAYS_TO_MAT_DATE"]=getParamEx(classCode,sec,"DAYS_TO_MAT_DATE").param_value+0, + ["BID"]=getParamEx(classCode,sec,"BID").param_value+0, + ["OFFER"]=getParamEx(classCode,sec,"OFFER").param_value+0, + ["OPTIONBASE"]=getParamEx(classCode,sec,"optionbase").param_image, + ["OPTIONTYPE"]=getParamEx(classCode,sec,"optiontype").param_image, + ["Longname"]=getParamEx(classCode,sec,"longname").param_image, + ["shortname"]=getParamEx(classCode,sec,"shortname").param_image, + ["Volatility"]=getParamEx(classCode,sec,"volatility").param_value+0, + ["Strike"]=getParamEx(classCode,sec,"strike").param_value+0 + } + + + + table.insert( t, p ) + end + +end +return t +end + +-------------------------- +-- Stop order functions -- +-------------------------- + +--- Возвращает список стоп-заявок +function qsfunctions.get_stop_orders(msg) + if msg.data ~= "" then + local spl = split(msg.data, "|") + local class_code, sec_code = spl[1], spl[2] + end + + local count = getNumberOf("stop_orders") + local stop_orders = {} + for i = 0, count - 1 do + local stop_order = getItem("stop_orders", i) + if msg.data == "" or (stop_order.class_code == class_code and stop_order.sec_code == sec_code) then + table.insert(stop_orders, stop_order) + end + end + msg.data = stop_orders + return msg +end + +------------------------- +--- Candles functions --- +------------------------- + +--- Возвращаем количество свечей по тегу +function qsfunctions.get_num_candles(msg) + log("Called get_num_candles" .. msg.data, 2) + local spl = split(msg.data, "|") + local tag = spl[1] + + msg.data = getNumCandles(tag) * 1 + return msg +end + + +--- Возвращаем все свечи по идентификатору графика. График должен быть открыт +function qsfunctions.get_candles(msg) + log("Called get_candles" .. msg.data, 2) + local spl = split(msg.data, "|") + local tag = spl[1] + local line = tonumber(spl[2]) + local first_candle = tonumber(spl[3]) + local count = tonumber(spl[4]) + if count == 0 then + count = getNumCandles(tag) * 1 + end + log("Count: " .. count, 2) + local t,n,l = getCandlesByIndex(tag, line, first_candle, count) + log("Candles table size: " .. n, 2) + log("Label: " .. l, 2) + local candles = {} + for i = 0, count - 1 do + table.insert(candles, t[i]) + end + msg.data = candles + return msg +end + +--- Возвращаем все свечи по заданному инструменту и интервалу +--- (ichechet) Если исторические данные по тикеру не приходят, то QUIK блокируется. Чтобы это не происходило, вводим таймаут +function qsfunctions.get_candles_from_data_source(msg) + local ds, is_error = create_data_source(msg) + if not is_error then + --- Источник данных изначально приходит пустым. Нужно подождать пока он заполнится данными. Бесконечно ждать тоже нельзя. Вводим таймаут + local s = 0 --- Будем ждать 5 секунд, прежде чем вернем таймаут + repeat --- Ждем + sleep(100) --- 100 миллисекунд + s = s + 100 --- Запоминаем кол-во прошедших миллисекунд + until (ds:Size() > 0 or s > 5000) --- До тех пор, пока не придут данные или пока не наступит таймаут + + local count = tonumber(split(msg.data, "|")[4]) --- возвращаем последние count свечей. Если равен 0, то возвращаем все доступные свечи. + local class, sec, interval = get_candles_param(msg) + local candles = {} + local start_i = count == 0 and 1 or math.max(1, ds:Size() - count + 1) + for i = start_i, ds:Size() do + local candle = fetch_candle(ds, i) + candle.sec = sec + candle.class = class + candle.interval = interval + table.insert(candles, candle) + end + ds:Close() + msg.data = candles + end + return msg +end + +function create_data_source(msg) + local class, sec, interval = get_candles_param(msg) + local ds, error_descr = CreateDataSource(class, sec, interval) + local is_error = false + if(error_descr ~= nil) then + msg.cmd = "lua_create_data_source_error" + msg.lua_error = error_descr + is_error = true + elseif ds == nil then + msg.cmd = "lua_create_data_source_error" + msg.lua_error = "Can't create data source for " .. class .. ", " .. sec .. ", " .. tostring(interval) + is_error = true + end + return ds, is_error +end + +function fetch_candle(data_source, index) + local candle = {} + candle.low = data_source:L(index) + candle.close = data_source:C(index) + candle.high = data_source:H(index) + candle.open = data_source:O(index) + candle.volume = data_source:V(index) + candle.datetime = data_source:T(index) + return candle +end + +--- Словарь открытых подписок (datasources) на свечи +data_sources = {} +last_indexes = {} + +--- Подписаться на получения свечей по заданному инструмент и интервалу +function qsfunctions.subscribe_to_candles(msg) + local ds, is_error = create_data_source(msg) + if not is_error then + local class, sec, interval = get_candles_param(msg) + local key = get_key(class, sec, interval) + data_sources[key] = ds + last_indexes[key] = ds:Size() + ds:SetUpdateCallback( + function(index) + data_source_callback(index, class, sec, interval) + end) + end + return msg +end + +function data_source_callback(index, class, sec, interval) + local key = get_key(class, sec, interval) + if index ~= last_indexes[key] then + last_indexes[key] = index + + local candle = fetch_candle(data_sources[key], index - 1) + candle.sec = sec + candle.class = class + candle.interval = interval + + local msg = {} + msg.t = timemsec() + msg.cmd = "NewCandle" + msg.data = candle + sendCallback(msg) + end +end + +--- Отписать от получения свечей по заданному инструменту и интервалу +function qsfunctions.unsubscribe_from_candles(msg) + local class, sec, interval = get_candles_param(msg) + local key = get_key(class, sec, interval) + data_sources[key]:Close() + data_sources[key] = nil + last_indexes[key] = nil + return msg +end + +--- Проверить открыта ли подписка на заданный инструмент и интервал +function qsfunctions.is_subscribed(msg) + local class, sec, interval = get_candles_param(msg) + local key = get_key(class, sec, interval) + for k, v in pairs(data_sources) do + if key == k then + msg.data = true; + return msg + end + end + msg.data = false + return msg +end + +--- Возвращает из msg информацию о инструменте на который подписываемся и интервале +function get_candles_param(msg) + local spl = split(msg.data, "|") + return spl[1], spl[2], tonumber(spl[3]) +end + +--- Возвращает уникальный ключ для инструмента на который подписываемся и инетрвала +function get_key(class, sec, interval) + return class .. "|" .. sec .. "|" .. tostring(interval) +end + +------------------------- +--- UCP functions --- +------------------------- + +--- Функция возвращает торговый счет срочного рынка, соответствующий коду клиента фондового рынка с единой денежной позицией +function qsfunctions.GetTrdAccByClientCode(msg) + local spl = split(msg.data, "|") + local firmId, clientCode = spl[1], spl[2] + msg.data = getTrdAccByClientCode(firmId, clientCode) + return msg +end + +--- Функция возвращает код клиента фондового рынка с единой денежной позицией, соответствующий торговому счету срочного рынка +function qsfunctions.GetClientCodeByTrdAcc(msg) + local spl = split(msg.data, "|") + local firmId, trdAccId = spl[1], spl[2] + msg.data = getClientCodeByTrdAcc(firmId, trdAccId) + return msg +end + +--- Функция предназначена для получения признака, указывающего имеет ли клиент единую денежную позицию +function qsfunctions.IsUcpClient(msg) + local spl = split(msg.data, "|") + local firmId, client = spl[1], spl[2] + msg.data = isUcpClient(firmId, client) + return msg +end + +return qsfunctions diff --git a/QUIK/lua/qsutils.lua b/QUIK/lua/qsutils.lua new file mode 100644 index 0000000..54e6a05 --- /dev/null +++ b/QUIK/lua/qsutils.lua @@ -0,0 +1,295 @@ +--~ Copyright (c) 2014-2020 QUIKSharp Authors https://github.com/finsight/QUIKSharp/blob/master/AUTHORS.md. All rights reserved. +--~ Licensed under the Apache License, Version 2.0. See LICENSE.txt in the project root for license information. + +local socket = require ("socket") +local json = require ("dkjson") +local qsutils = {} + +--- Sleep that always works +function delay(msec) + if sleep then + pcall(sleep, msec) + else + -- pcall(socket.select, nil, nil, msec / 1000) + end +end + +-- high precision current time +function timemsec() + local st, res = pcall(socket.gettime) + if st then + return (res) * 1000 + else + log("unexpected error in timemsec", 3) + error("unexpected error in timemsec") + end +end + +-- Returns the name of the file that calls this function (without extension) +function scriptFilename() + -- Check that Lua runtime was built with debug information enabled + if not debug or not debug.getinfo then + return nil + end + local full_path = debug.getinfo(2, "S").source:sub(2) + return string.gsub(full_path, "^.*\\(.*)[.]lua[c]?$", "%1") +end + +-- when true will show QUIK message for log(...,0) +is_debug = false + +-- log files + +function openLog() + os.execute("mkdir \""..script_path.."\\logs\"") + local lf = io.open (script_path.. "\\logs\\QUIK#_"..os.date("%Y%m%d")..".log", "a") + if not lf then + lf = io.open (script_path.. "\\QUIK#_"..os.date("%Y%m%d")..".log", "a") + end + return lf +end + +-- Returns contents of config.json file or nil if no such file exists +function readConfigAsJson() + local conf = io.open (script_path.. "\\config.json", "r") + if not conf then + return nil + end + local content = conf:read "*a" + conf:close() + return from_json(content) +end + +function paramsFromConfig(scriptName) + local params = {} + -- just default values + table.insert(params, "127.0.0.1") -- responseHostname + table.insert(params, 34130) -- responsePort + table.insert(params, "127.0.0.1") -- callbackHostname + table.insert(params, 34131) -- callbackPort + + local config = readConfigAsJson() + if not config or not config.servers then + return nil + end + local found = false + for i=1,#config.servers do + local server = config.servers[i] + if server.scriptName == scriptName then + found = true + if server.responseHostname then + params[1] = server.responseHostname + end + if server.responsePort then + params[2] = server.responsePort + end + if server.callbackHostname then + params[3] = server.callbackHostname + end + if server.callbackPort then + params[4] = server.callbackPort + end + end + end + + if found then + return params + else + return nil + end +end + +-- closes log +function closeLog() + if logfile then + pcall(logfile:close(logfile)) + end +end + +logfile = openLog() + +--- Write to log file and to Quik messages +function log(msg, level) + if not msg then msg = "" end + if level == 1 or level == 2 or level == 3 or is_debug then + -- only warnings and recoverable errors to Quik + if message then + pcall(message, msg, level) + end + end + if not level then level = 0 end + local logLine = "LOG "..level..": "..msg + print(logLine) + local msecs = math.floor(math.fmod(timemsec(), 1000)); + if logfile then + pcall(logfile.write, logfile, os.date("%Y-%m-%d %H:%M:%S").."."..msecs.." "..logLine.."\n") + pcall(logfile.flush, logfile) + end +end + + +function split(inputstr, sep) + if sep == nil then + sep = "%s" + end + local t={} + local i=1 + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + t[i] = str + i = i + 1 + end + return t +end + +function from_json(str) + local status, msg= pcall(json.decode, str, 1, json.null) -- dkjson + if status then + return msg + else + return nil, msg + end +end + +function to_json(msg) + local status, str= pcall(json.encode, msg, { indent = false }) -- dkjson + if status then + return str + else + error(str) + end +end + +-- current connection state +is_connected = false +local response_server +local callback_server +local response_client +local callback_client + +--- accept client on server +local function getResponseServer() + log('Waiting for a response client...', 0) + if not response_server then + log("Cannot bind to response_server, probably the port is already in use", 3) + else + while true do + local status, client, err = pcall(response_server.accept, response_server ) + if status and client then + return client + else + log(err, 3) + end + end + end +end + +local function getCallbackClient() + log('Waiting for a callback client...', 0) + if not callback_server then + log("Cannot bind to callback_server, probably the port is already in use", 3) + else + while true do + local status, client, err = pcall(callback_server.accept, callback_server) + if status and client then + return client + else + log(err, 3) + end + end + end +end + +function qsutils.connect(response_host, response_port, callback_host, callback_port) + if not response_server then + response_server = socket.bind(response_host, response_port, 1) + end + if not callback_server then + callback_server = socket.bind(callback_host, callback_port, 1) + end + + if not is_connected then + log('QUIK# is waiting for client connection...', 1) + if response_client then + log("is_connected is false but the response client is not nil", 3) + -- Quik crashes without pcall + pcall(response_client.close, response_client) + end + if callback_client then + log("is_connected is false but the callback client is not nil", 3) + -- Quik crashes without pcall + pcall(callback_client.close, callback_client) + end + response_client = getResponseServer() + callback_client = getCallbackClient() + if response_client and callback_client then + is_connected = true + log('QUIK# client connected', 1) + end + end +end + +local function disconnected() + is_connected = false + log('QUIK# client disconnected', 1) + if response_client then + pcall(response_client.close, response_client) + response_client = nil + end + if callback_client then + pcall(callback_client.close, callback_client) + callback_client = nil + end + OnQuikSharpDisconnected() +end + +--- get a decoded message as a table +function receiveRequest() + if not is_connected then + return nil, "not conencted" + end + local status, requestString= pcall(response_client.receive, response_client) + if status and requestString then + local msg_table, err = from_json(requestString) + if err then + log(err, 3) + return nil, err + else + return msg_table + end + else + disconnected() + return nil, err + end +end + +function sendResponse(msg_table) + -- if not set explicitly then set CreatedTime "t" property here + -- if not msg_table.t then msg_table.t = timemsec() end + local responseString = to_json(msg_table) + if is_connected then + local status, res = pcall(response_client.send, response_client, responseString..'\n') + if status and res then + return true + else + disconnected() + return nil, err + end + end +end + +function sendCallback(msg_table) + -- if not set explicitly then set CreatedTime "t" property here + -- if not msg_table.t then msg_table.t = timemsec() end + local callback_string = to_json(msg_table) + if is_connected then + local status, res = pcall(callback_client.send, callback_client, callback_string..'\n') + if status and res then + return true + else + disconnected() + return nil, err + end + end +end + +return qsutils diff --git a/QUIK/lua/socket.lua b/QUIK/lua/socket.lua new file mode 100644 index 0000000..65cb709 --- /dev/null +++ b/QUIK/lua/socket.lua @@ -0,0 +1,149 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") + +local _M = socket + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function _M.connect4(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet") +end + +function _M.connect6(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet6") +end + +function _M.bind(host, port, backlog) + if host == "*" then host = "0.0.0.0" end + local addrinfo, err = socket.dns.getaddrinfo(host); + if not addrinfo then return nil, err end + local sock, res + err = "no info on address" + for i, alt in base.ipairs(addrinfo) do + if alt.family == "inet" then + sock, err = socket.tcp4() + else + sock, err = socket.tcp6() + end + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + res, err = sock:bind(alt.addr, port) + if not res then + sock:close() + else + res, err = sock:listen(backlog) + if not res then + sock:close() + else + return sock + end + end + end + return nil, err +end + +_M.try = _M.newtry() + +function _M.choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +local sourcet, sinkt = {}, {} +_M.sourcet = sourcet +_M.sinkt = sinkt + +_M.BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +_M.sink = _M.choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +_M.source = _M.choose(sourcet) + +return _M \ No newline at end of file diff --git a/QUIK/socket/core.dll b/QUIK/socket/core.dll new file mode 100644 index 0000000..5def8ef Binary files /dev/null and b/QUIK/socket/core.dll differ diff --git a/QuikPy.py b/QuikPy.py new file mode 100644 index 0000000..d372f6d --- /dev/null +++ b/QuikPy.py @@ -0,0 +1,588 @@ +import socket # Обращаться к LUA скриптам QuikSharp будем через соединения +import threading # Результат работы функций обратного вызова будем получать в отдельном потоке +import json # Передавать и принимать данные в QUIK будем через JSON +import time # Задержка при чтении буфера + + +class Singleton(type): + """Метакласс для создания Singleton классов""" + def __init__(cls, *args, **kwargs): + """Инициализация класса""" + super(Singleton, cls).__init__(*args, **kwargs) + cls._singleton = None # Экземпляра класса еще нет + + def __call__(cls, *args, **kwargs): + """Вызов класса""" + if cls._singleton is None: # Если класса нет в экземплярах класса + cls._singleton = super(Singleton, cls).__call__(*args, **kwargs) + return cls._singleton # Возвращаем экземпляр класса + + +class QuikPy(metaclass=Singleton): # Singleton класс + """Работа с Quik из Python через LUA скрипты QuikSharp https://github.com/finsight/QUIKSharp/tree/master/src/QuikSharp/lua + На основе Документации по языку LUA в QUIK из https://arqatech.com/ru/support/files/ + """ + bufferSize = 4096 # Размер буфера приема в байтах + socketRequests = None # Соединение для запросов + callbackThread = None # Поток обработки функций обратного вызова + + def DefaultHandler(self, data): + """Пустой обработчик события по умолчанию. Его можно заменить на пользовательский""" + pass + + def CallbackHandler(self): + """Поток обработки результатов функций обратного вызова""" + # TODO Проверить работу при переходе на следующую сессию (отключение и включение QUIK) + socketCallbacks = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Соединение для функций обратного вызова + socketCallbacks.connect((self.Host, self.CallbacksPort)) # Открываем соединение для функций обратного вызова + currentThread = threading.currentThread() # Получаем текущий поток + while getattr(currentThread, 'process', True): # Пока поток нужен + fragments = [] # Гораздо быстрее получать ответ в виде списка фрагментов + while True: # Пока есть что-то в буфере ответов + fragment = socketCallbacks.recv(self.bufferSize) # Читаем фрагмент из буфера + fragments.append(fragment.decode('cp1251')) # Переводим фрагмент в Windows кодировку 1251, добавляем в список + if len(fragment) < self.bufferSize: # Если в принятом фрагменте данных меньше чем размер буфера + break # то это был последний фрагмент, выходим из чтения буфера + time.sleep(0.000001) # Может так получиться, что буфер будет считываться быстрее, чем заполняться. Поэтому, ставим небольшую задержку перед следующим чтением + data = ''.join(fragments) # Собираем список фрагментов в строку + dataList = data.split('\n') # Одновременно могут прийти несколько функций обратного вызова, разбираем их по одной + for data in dataList: # Пробегаемся по всем функциям обратного вызова + if data == '': # Если функция обратного вызова пустая + continue # то ее не разбираем, переходим на следующую функцию, дальше не продолжаем + data = json.loads(data) # Возвращаем полученный ответ в формате JSON + # Разбираем функцию обратного вызова QUIK LUA + if data['cmd'] == 'OnFirm': # 1. Новая фирма + self.OnFirm(data) + elif data['cmd'] == 'OnAllTrade': # 2. Получение обезличенной сделки + self.OnAllTrade(data) + elif data['cmd'] == 'OnTrade': # 3. Получение новой / изменение существующей сделки + self.OnTrade(data) + elif data['cmd'] == 'OnOrder': # 4. Получение новой / изменение существующей заявки + self.OnOrder(data) + elif data['cmd'] == 'OnAccountBalance': # 5. Изменение позиций по счету + self.OnAccountBalance(data) + elif data['cmd'] == 'OnFuturesLimitChange': # 6. Изменение ограничений по срочному рынку + self.OnFuturesLimitChange(data) + elif data['cmd'] == 'OnFuturesLimitDelete': # 7. Удаление ограничений по срочному рынку + self.OnFuturesLimitDelete(data) + elif data['cmd'] == 'OnFuturesClientHolding': # 8. Изменение позиции по срочному рынку + self.OnFuturesClientHolding(data) + elif data['cmd'] == 'OnMoneyLimit': # 9. Изменение денежной позиции + self.OnMoneyLimit(data) + elif data['cmd'] == 'OnMoneyLimitDelete': # 10. Удаление денежной позиции + self.OnMoneyLimitDelete(data) + elif data['cmd'] == 'OnDepoLimit': # 11. Изменение позиций по инструментам + self.OnDepoLimit(data) + elif data['cmd'] == 'OnDepoLimitDelete': # 12. Удаление позиции по инструментам + self.OnDepoLimitDelete(data) + elif data['cmd'] == 'OnAccountPosition': # 13. Изменение денежных средств + self.OnAccountPosition(data) + # OnNegDeal - 14. Получение новой / изменение существующей внебиржевой заявки + # OnNegTrade - 15. Получение новой / изменение существующей сделки для исполнения + elif data['cmd'] == 'OnStopOrder': # 16. Получение новой / изменение существующей стоп-заявки + self.OnStopOrder(data) + elif data['cmd'] == 'OnTransReply': # 17. Ответ на транзакцию пользователя + self.OnTransReply(data) + elif data['cmd'] == 'OnParam': # 18. Изменение текущих параметров + self.OnParam(data) + elif data['cmd'] == 'OnQuote': # 19. Изменение стакана котировок + self.OnQuote(data) + elif data['cmd'] == 'OnDisconnected': # 20. Отключение терминала от сервера QUIK + self.OnDisconnected(data) + elif data['cmd'] == 'OnConnected': # 21. Соединение терминала с сервером QUIK + self.OnConnected(data) + # OnCleanUp - 22. Смена сервера QUIK / Пользователя / Сессии + elif data['cmd'] == 'OnClose': # 23. Закрытие терминала QUIK + self.OnClose(data) + elif data['cmd'] == 'OnStop': # 24. Остановка LUA скрипта в терминале QUIK / закрытие терминала QUIK + self.OnStop(data) + elif data['cmd'] == 'OnInit': # 25. Запуск LUA скрипта в терминале QUIK + self.OnInit(data) + # Разбираем функции обратного вызова QuikSharp + elif data['cmd'] == 'NewCandle': # Получение новой свечки + self.OnNewCandle(data) + elif data['cmd'] == 'OnError': # Получено сообщение об ошибке + self.OnError(data) + socketCallbacks.close() # Закрываем соединение для ответов + + def ProcessRequest(self, Request): + """Отправляем запрос в QUIK, получаем ответ из QUIK""" + rawData = json.dumps(Request) # Переводим запрос в формат JSON + self.socketRequests.sendall(f'{rawData}\r\n'.encode()) # Отправляем запрос в QUIK + fragments = [] # Гораздо быстрее получать ответ в виде списка фрагментов + while True: # Пока фрагменты есть в буфере + fragment = self.socketRequests.recv(self.bufferSize) # Читаем фрагмент из буфера + fragments.append(fragment.decode('cp1251')) # Переводим фрагмент в Windows кодировку 1251, добавляем в список + if len(fragment) < self.bufferSize: # Если в принятом фрагменте данных меньше чем размер буфера + break # то это был последний фрагмент, выходим из чтения буфера + time.sleep(0.000001) # Может так получиться, что буфер будет считываться быстрее, чем заполняться. Поэтому, ставим небольшую задержку перед следующим чтением + data = ''.join(fragments) # Собираем список фрагментов в строку + return json.loads(data) # Возвращаем ответ в формате JSON в Windows кодировке 1251 + + # Инициализация и вход + + def __init__(self, Host='127.0.0.1', RequestsPort=34130, CallbacksPort=34131): + """Инициализация""" + # 2.2. Функции обратного вызова + self.OnFirm = self.DefaultHandler # 1. Новая фирма + self.OnAllTrade = self.DefaultHandler # 2. Получение обезличенной сделки + self.OnTrade = self.DefaultHandler # 3. Получение новой / изменение существующей сделки + self.OnOrder = self.DefaultHandler # 4. Получение новой / изменение существующей заявки + self.OnAccountBalance = self.DefaultHandler # 5. Изменение позиций + self.OnFuturesLimitChange = self.DefaultHandler # 6. Изменение ограничений по срочному рынку + self.OnFuturesLimitDelete = self.DefaultHandler # 7. Удаление ограничений по срочному рынку + self.OnFuturesClientHolding = self.DefaultHandler # 8. Изменение позиции по срочному рынку + self.OnMoneyLimit = self.DefaultHandler # 9. Изменение денежной позиции + self.OnMoneyLimitDelete = self.DefaultHandler # 10. Удаление денежной позиции + self.OnDepoLimit = self.DefaultHandler # 11. Изменение позиций по инструментам + self.OnDepoLimitDelete = self.DefaultHandler # 12. Удаление позиции по инструментам + self.OnAccountPosition = self.DefaultHandler # 13. Изменение денежных средств + # OnNegDeal - 14. Получение новой / изменение существующей внебиржевой заявки + # OnNegTrade - 15. Получение новой / изменение существующей сделки для исполнения + self.OnStopOrder = self.DefaultHandler # 16. Получение новой / изменение существующей стоп-заявки + self.OnTransReply = self.DefaultHandler # 17. Ответ на транзакцию пользователя + self.OnParam = self.DefaultHandler # 18. Изменение текущих параметров + self.OnQuote = self.DefaultHandler # 19. Изменение стакана котировок + self.OnDisconnected = self.DefaultHandler # 20. Отключение терминала от сервера QUIK + self.OnConnected = self.DefaultHandler # 21. Соединение терминала с сервером QUIK + # OnCleanUp - 22. Смена сервера QUIK / Пользователя / Сессии + self.OnClose = self.DefaultHandler # 23. Закрытие терминала QUIK + self.OnStop = self.DefaultHandler # 24. Остановка LUA скрипта в терминале QUIK / закрытие терминала QUIK + self.OnInit = self.DefaultHandler # 25. Запуск LUA скрипта в терминале QUIK + + # Функции обратного вызова QuikSharp + self.OnNewCandle = self.DefaultHandler # Получение новой свечки + self.OnError = self.DefaultHandler # Получено сообщение об ошибке + + self.Host = Host # IP адрес или название хоста + self.RequestsPort = RequestsPort # Порт для отправки запросов и получения ответов + self.CallbacksPort = CallbacksPort # Порт для функций обратного вызова + self.socketRequests = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Создаем соединение для запросов + self.socketRequests.connect((self.Host, self.RequestsPort)) # Открываем соединение для запросов + + self.callbackThread = threading.Thread(target=self.CallbackHandler, name='CallbackThread') # Создаем поток обработки функций обратного вызова + self.callbackThread.start() # Запускаем поток + + def __enter__(self): + """Вход в класс, например, с with""" + return self + + # Фукнции связи с QuikSharp + + def Ping(self, TransId=0): + """Проверка соединения. Отправка ping. Получение pong""" + return self.ProcessRequest({'data': 'Ping', 'id': TransId, 'cmd': 'ping', 't': ''}) + + def Echo(self, Message, TransId=0): + """Эхо. Отправка и получение одного и того же сообщения""" + return self.ProcessRequest({'data': Message, 'id': TransId, 'cmd': 'echo', 't': ''}) + + def DivideStringByZero(self, TransId=0): + """Тест обработки ошибок. Выполняется деление на 0 с выдачей ошибки""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'divide_string_by_zero', 't': ''}) + + def IsQuik(self, TransId=0): + """Скрипт запущен в Квике""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'is_quik', 't': ''}) + + # 2.1 Сервисные функции + + def IsConnected(self, TransId=0): # 1 + """Состояние подключения терминала к серверу QUIK. Возвращает 1 - подключено / 0 - не подключено""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'isConnected', 't': ''}) + + def GetScriptPath(self, TransId=0): # 2 + """Путь скрипта без завершающего обратного слэша""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getScriptPath', 't': ''}) + + def GetInfoParam(self, Params, TransId=0): # 3 + """Значения параметров информационного окна""" + return self.ProcessRequest({'data': Params, 'id': TransId, 'cmd': 'getInfoParam', 't': ''}) + + # message - 4. Сообщение в терминале QUIK. Реализовано в виде 3-х отдельных функций в QuikSharp + + def Sleep(self, Time, TransId=0): # 5 + """Приостановка скрипта. Время в миллисекундах""" + return self.ProcessRequest({'data': Time, 'id': TransId, 'cmd': 'sleep', 't': ''}) + + def GetWorkingFolder(self, TransId=0): # 6 + """Путь к info.exe, исполняющего скрипт без завершающего обратного слэша""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getWorkingFolder', 't': ''}) + + def PrintDbgStr(self, Message, TransId=0): # 7 + """Вывод отладочной информации. Можно посмотреть с помощью DebugView""" + return self.ProcessRequest({'data': Message, 'id': TransId, 'cmd': 'PrintDbgStr', 't': ''}) + + # sysdate - 8. Системные дата и время + # isDarkTheme - 9. Тема оформления. true - тёмная, false - светлая + + # Сервисные функции QuikSharp + + def MessageInfo(self, Message, TransId=0): # В QUIK LUA message icon_type=1 + """Отправка информационного сообщения в терминал QUIK""" + return self.ProcessRequest({'data': Message, 'id': TransId, 'cmd': 'message', 't': ''}) + + def MessageWarning(self, Message, TransId=0): # В QUIK LUA message icon_type=2 + """Отправка сообщения с предупреждением в терминал QUIK""" + return self.ProcessRequest({'data': Message, 'id': TransId, 'cmd': 'warning_message', 't': ''}) + + def MessageError(self, Message, TransId=0): # В QUIK LUA message icon_type=3 + """Отправка сообщения об ошибке в терминал QUIK""" + return self.ProcessRequest({'data': Message, 'id': TransId, 'cmd': 'error_message', 't': ''}) + + # 3.1. Функции для обращения к строкам произвольных таблиц + + # getItem - 1. Строка таблицы + # getOrderByNumber - 2. Заявка + # getNumberOf - 3. Кол-во записей в таблице + # SearchItems - 4. Быстрый поиск по таблице заданной функцией поиска + + # Функции для обращения к строкам произвольных таблиц QuikSharp + + def GetTradeAccounts(self, TransId=0): + """Торговые счета, у которых указаны поддерживаемые классы инструментов""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getTradeAccounts', 't': ''}) + + def GetTradeAccount(self, ClassCode, TransId=0): + """Торговый счет для запрашиваемого кода класса""" + return self.ProcessRequest({'data': ClassCode, 'id': TransId, 'cmd': 'getTradeAccount', 't': ''}) + + def GetAllOrders(self, TransId=0): + """Таблица заявок (вся)""" + return self.ProcessRequest({'data': f'', 'id': TransId, 'cmd': 'get_orders', 't': ''}) + + def GetOrders(self, ClassCode, SecCode, TransId=0): + """Таблица заявок (по инструменту)""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'get_orders', 't': ''}) + + def GetOrderByNumber(self, OrderId, TransId=0): + """Заявка по номеру""" + return self.ProcessRequest({'data': OrderId, 'id': TransId, 'cmd': 'getOrder_by_Number', 't': ''}) + + def GetOrderById(self, ClassCode, SecCode, OrderTransId, TransId=0): + """Заявка по инструменту и Id транзакции""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{OrderTransId}', 'id': TransId, 'cmd': 'getOrder_by_ID', 't': ''}) + + def GetOrderByClassNumber(self, ClassCode, OrderId, TransId=0): + """Заявка по классу инструмента и номеру""" + return self.ProcessRequest({'data': f'{ClassCode}|{OrderId}', 'id': TransId, 'cmd': 'getOrder_by_Number', 't': ''}) + + def GetMoneyLimits(self, TransId=0): + """Все денежные лимиты""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getMoneyLimits', 't': ''}) + + def GetClientCode(self, TransId=0): + """Основной (первый) код клиента""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getClientCode', 't': ''}) + + def GetClientCodes(self, TransId=0): + """Все коды клиента""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getClientCode', 't': ''}) + + def GetAllDepoLimits(self, TransId=0): + """Лимиты по бумагам (всем)""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'get_depo_limits', 't': ''}) + + def GetDepoLimits(self, SecCode, TransId=0): + """Лимиты по бумагам (по инструменту)""" + return self.ProcessRequest({'data': SecCode, 'id': TransId, 'cmd': 'get_depo_limits', 't': ''}) + + def GetAllTrades(self, TransId=0): + """Таблица сделок (вся)""" + return self.ProcessRequest({'data': f'', 'id': TransId, 'cmd': 'get_trades', 't': ''}) + + def GetTrades(self, ClassCode, SecCode, TransId=0): + """Таблица сделок (по инструменту)""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'get_trades', 't': ''}) + + def GetTradesByOrderNumber(self, OrderNum, TransId=0): + """Таблица сделок по номеру заявки""" + return self.ProcessRequest({'data': OrderNum, 'id': TransId, 'cmd': 'get_Trades_by_OrderNumber', 't': ''}) + + def GetAllStopOrders(self, TransId=0): + """Стоп заявки (все)""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'get_stop_orders', 't': ''}) + + def GetStopOrders(self, ClassCode, SecCode, TransId=0): + """Стоп заявки (по инструменту)""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'get_stop_orders', 't': ''}) + + def GetAllTrade(self, TransId=0): + """Таблица обезличенных сделок (вся)""" + return self.ProcessRequest({'data': f'', 'id': TransId, 'cmd': 'get_all_trades', 't': ''}) + + def GetTrade(self, ClassCode, SecCode, TransId=0): + """Таблица обезличенных сделок (по инструменту)""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'get_all_trades', 't': ''}) + + # 3.2 Функции для обращения к спискам доступных параметров + + def GetClassesList(self, TransId=0): # 1 + """Список классов""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getClassesList', 't': ''}) + + def GetClassInfo(self, ClassCode, TransId=0): # 2 + """Информация о классе""" + return self.ProcessRequest({'data': ClassCode, 'id': TransId, 'cmd': 'getClassInfo', 't': ''}) + + def GetClassSecurities(self, ClassCode, TransId=0): # 3 + """Список инструментов класса""" + return self.ProcessRequest({'data': ClassCode, 'id': TransId, 'cmd': 'getClassSecurities', 't': ''}) + + # Функции для обращения к спискам доступных параметров QuikSharp + + def GetOptionBoard(self, ClassCode, SecCode, TransId=0): + """Доска опционов""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'getOptionBoard', 't': ''}) + + # 3.3 Функции для получения информации по денежным средствам + + def GetMoney(self, ClientCode, FirmId, Tag, CurrCode, TransId=0): # 1 + """Денежные позиции""" + return self.ProcessRequest({'data': f'{ClientCode}|{FirmId}|{Tag}|{CurrCode}', 'id': TransId, 'cmd': 'getMoney', 't': ''}) + + def GetMoneyEx(self, FirmId, ClientCode, Tag, CurrCode, LimitKind, TransId=0): # 2 + """Денежные позиции указанного типа""" + return self.ProcessRequest({'data': f'{FirmId}|{ClientCode}|{Tag}|{CurrCode}|{LimitKind}', 'id': TransId, 'cmd': 'getMoneyEx', 't': ''}) + + # 3.4 Функции для получения позиций по инструментам + + def GetDepo(self, ClientCode, FirmId, SecCode, Account, TransId=0): # 1 + """Позиции по инструментам""" + return self.ProcessRequest({'data': f'{ClientCode}|{FirmId}|{SecCode}|{Account}', 'id': TransId, 'cmd': 'getDepo', 't': ''}) + + def GetDepoEx(self, FirmId, ClientCode, SecCode, Account, LimitKind, TransId=0): # 2 + """Позиции по инструментам указанного типа""" + return self.ProcessRequest({'data': f'{FirmId}|{ClientCode}|{SecCode}|{Account}|{LimitKind}', 'id': TransId, 'cmd': 'getDepoEx', 't': ''}) + + # 3.5 Функция для получения информации по фьючерсным лимитам + + def GetFuturesLimit(self, FirmId, AccountId, LimitType, CurrCode, TransId=0): # 1 + """Фьючерсные лимиты""" + return self.ProcessRequest({'data': f'{FirmId}|{AccountId}|{LimitType}|{CurrCode}', 'id': TransId, 'cmd': 'getFuturesLimit', 't': ''}) + + # Функция для получения информации по фьючерсным лимитам QuikSharp + + def GetFuturesClientLimits(self, TransId=0): + """Все фьючерсные лимиты""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getFuturesClientLimits', 't': ''}) + + # 3.6 Функция для получения информации по фьючерсным позициям + + def GetFuturesHolding(self, FirmId, AccountId, SecCode, PositionType, TransId=0): # 1 + """Фьючерсные позиции""" + return self.ProcessRequest({'data': f'{FirmId}|{AccountId}|{SecCode}|{PositionType}', 'id': TransId, 'cmd': 'getFuturesHolding', 't': ''}) + + # Функция для получения информации по фьючерсным позициям QuikSharp + + def GetFuturesHoldings(self, TransId=0): + """Все фьючерсные позиции""" + return self.ProcessRequest({'data': '', 'id': TransId, 'cmd': 'getFuturesHolding', 't': ''}) + + # 3.7 Функция для получения информации по инструменту + + def GetSecurityInfo(self, ClassCode, SecCode, TransId=0): # 1 + """Информация по инструменту""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'getSecurityInfo', 't': ''}) + + # Функция для получения информации по инструменту QuikSharp + + def GetSecurityInfoBulk(self, ClassCodes, SecCodes, TransId=0): + """Информация по инструментам""" + return self.ProcessRequest({'data': f'{ClassCodes}|{SecCodes}', 'id': TransId, 'cmd': 'getSecurityInfoBulk', 't': ''}) + + def GetSecurityClass(self, ClassesList, SecCode, TransId=0): + """Класс по коду инструмента из заданных классов""" + return self.ProcessRequest({'data': f'{ClassesList}|{SecCode}', 'id': TransId, 'cmd': 'getSecurityClass', 't': ''}) + + # 3.8 Функция для получения даты торговой сессии + + # getTradeDate - 1. Дата текущей торговой сессии + + # 3.9 Функция для получения стакана по указанному классу и инструменту + + def GetQuoteLevel2(self, ClassCode, SecCode, TransId=0): # 1 + """Стакан по классу и инструменту""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'GetQuoteLevel2', 't': ''}) + + # 3.10 Функции для работы с графиками + + # getLinesCount - 1. Кол-во линий в графике + + def GetNumCandles(self, Tag, TransId=0): # 2 + """Кол-во свечей по тэгу""" + return self.ProcessRequest({'data': Tag, 'id': TransId, 'cmd': 'getNumCandles', 't': ''}) + + # getCandlesByIndex - 3. Информация о свечках (реализовано в get_candles) + # CreateDataSource - 4. Создание источника данных c функциями: (реализовано в get_candles_from_data_source) + # - SetUpdateCallback - Привязка функции обратного вызова на изменение свечи + # - O, H, L, C, V, T - Функции получения цен, объемов и времени + # - Size - Функция кол-ва свечек в источнике данных + # - Close - Функция закрытия источника данных. Терминал прекращает получать данные с сервера + # - SetEmptyCallback - Функция сброса функции обратного вызова на изменение свечи + + # Функции для работы с графиками QuikSharp + + def GetCandles(self, Tag, Line, FirstCandle, Count, TransId=0): + """Свечки по идентификатору графика""" + return self.ProcessRequest({'data': f'{Tag}|{Line}|{FirstCandle}|{Count}', 'id': TransId, 'cmd': 'get_candles', 't': ''}) + + def GetCandlesFromDataSource(self, ClassCode, SecCode, Interval, Count): # ichechet - Добавлен выход по таймауту + """Свечки""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{Interval}|{Count}', 'id': '1', 'cmd': 'get_candles_from_data_source', 't': ''}) + + def SubscribeToCandles(self, ClassCode, SecCode, Interval, TransId=0): + """Подписка на свечки""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{Interval}', 'id': TransId, 'cmd': 'subscribe_to_candles', 't': ''}) + + def IsSubscribed(self, ClassCode, SecCode, Interval, TransId=0): + """Есть ли подписка на свечки""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{Interval}', 'id': TransId, 'cmd': 'is_subscribed', 't': ''}) + + def UnsubscribeFromCandles(self, ClassCode, SecCode, Interval, TransId=0): + """Отмена подписки на свечки""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{Interval}', 'id': TransId, 'cmd': 'unsubscribe_from_candles', 't': ''}) + + # 3.11 Функции для работы с заявками + + def SendTransaction(self, Transaction, TransId=0): # 1 + """Отправка транзакции в торговую систему""" + return self.ProcessRequest({'data': Transaction, 'id': TransId, 'cmd': 'sendTransaction', 't': ''}) + + # CalcBuySell - 2. Максимальное кол-во лотов в заявке + + # 3.12 Функции для получения значений таблицы "Текущие торги" + + def GetParamEx(self, ClassCode, SecCode, ParamName, TransId=0): # 1 + """Таблица текущих торгов""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{ParamName}', 'id': TransId, 'cmd': 'getParamEx', 't': ''}) + + def GetParamEx2(self, ClassCode, SecCode, ParamName, TransId=0): # 2 + """Таблица текущих торгов по инструменту с возможностью отказа от получения""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{ParamName}', 'id': TransId, 'cmd': 'getParamEx2', 't': ''}) + + # Функция для получения значений таблицы "Текущие торги" QuikSharp + + def GetParamEx2Bulk(self, ClassCodes, SecCodes, ParamNames, TransId=0): + """Таблица текущих торгов по инструментам с возможностью отказа от получения""" + return self.ProcessRequest({'data': f'{ClassCodes}|{SecCodes}|{ParamNames}', 'id': TransId, 'cmd': 'getParamEx2Bulk', 't': ''}) + + # 3.13 Функции для получения параметров таблицы "Клиентский портфель" + + def GetPortfolioInfo(self, FirmId, ClientCode, TransId=0): # 1 + """Клиентский портфель""" + return self.ProcessRequest({'data': f'{FirmId}|{ClientCode}', 'id': TransId, 'cmd': 'getPortfolioInfo', 't': ''}) + + def GetPortfolioInfoEx(self, FirmId, ClientCode, LimitKind, TransId=0): # 2 + """Клиентский портфель по сроку расчетов""" + return self.ProcessRequest({'data': f'{FirmId}|{ClientCode}|{LimitKind}', 'id': TransId, 'cmd': 'getPortfolioInfoEx', 't': ''}) + + # 3.14 Функции для получения параметров таблицы "Купить/Продать" + + # getBuySellInfo - 1. Параметры таблицы купить/продать + # getBuySellInfoEx - 2. Параметры таблицы купить/продать с дополнительными полями вывода + + # 3.15 Функции для работы с таблицами Рабочего места QUIK + + # AddColumn - 1. Добавление колонки в таблицу + # AllocTable - 2. Структура, описывающая таблицу + # Clear - 3. Удаление содержимого таблицы + # CreateWindow - 4. Создание окна таблицы + # DeleteRow - 5. Удаление строки из таблицы + # DestroyTable - 6. Закрытие окна таблицы + # InsertRow - 7. Добавление строки в таблицу + # IsWindowClosed - 8. Закрыто ли окно с таблицей + # GetCell - 9. Данные ячейки таблицы + # GetTableSize - 10. Кол-во строк и столбцов таблицы + # GetWindowCaption - 11. Заголовок окна таблицы + # GetWindowRect - 12. Координаты верхнего левого и правого нижнего углов таблицы + # SetCell - 13. Установка значения ячейки таблицы + # SetWindowCaption - 14. Установка заголовка окна таблицы + # SetWindowPos - 15. Установка верхнего левого угла, и размеры таблицы + # SetTableNotificationCallback - 16. Установка функции обратного вызова для обработки событий в таблице + # RGB - 17. Преобразование каждого цвета в одно число для функци SetColor + # SetColor - 18. Установка цвета ячейки, столбца или строки таблицы + # Highlight - 19. Подсветка диапазона ячеек цветом фона и цветом текста на заданное время с плавным затуханием + # SetSelectedRow - 20. Выделение строки таблицы + + # 3.16 Функции для работы с метками + + def AddLabel(self, Price, CurDate, CurTime, Qty, Path, LabelId, Alignment, Background, TransId=0): # 1 + """Добавление метки на график""" + return self.ProcessRequest({'data': f'{Price}|{CurDate}|{CurTime}|{Qty}|{Path}|{LabelId}|{Alignment}|{Background}', 'id': TransId, 'cmd': 'AddLabel', 't': ''}) + + def DelLabel(self, ChartTag, LabelId, TransId=0): # 2 + """Удаление метки с графика""" + return self.ProcessRequest({'data': f'{ChartTag}|{LabelId}', 'id': TransId, 'cmd': 'DelLabel', 't': ''}) + + def DelAllLabels(self, ChartTag, TransId=0): # 3 + """Удаление всех меток с графика""" + return self.ProcessRequest({'data': ChartTag, 'id': TransId, 'cmd': 'DelAllLabels', 't': ''}) + + def GetLabelParams(self, ChartTag, LabelId, TransId=0): # 4 + """Получение параметров метки""" + return self.ProcessRequest({'data': f'{ChartTag}|{LabelId}', 'id': TransId, 'cmd': 'GetLabelParams', 't': ''}) + + # SetLabelParams - 5. Установка параметров метки + + # 3.17 Функции для заказа стакана котировок + + def SubscribeLevel2Quotes(self, ClassCode, SecCode, TransId=0): # 1 + """Подписка на стакан по Классу|Коду бумаги""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'Subscribe_Level_II_Quotes', 't': ''}) + + def UnsubscribeLevel2Quotes(self, ClassCode, SecCode, TransId=0): # 2 + """Отмена подписки на стакан по Классу|Коду бумаги""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'Unsubscribe_Level_II_Quotes', 't': ''}) + + def IsSubscribedLevel2Quotes(self, ClassCode, SecCode, TransId=0): # 3 + """Есть ли подписка на стакан по Классу|Коду бумаги""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}', 'id': TransId, 'cmd': 'IsSubscribed_Level_II_Quotes', 't': ''}) + + # 3.18 Функции для заказа параметров Таблицы текущих торгов + + def ParamRequest(self, ClassCode, SecCode, ParamName, TransId=0): # 1 + """Заказ получения таблицы текущих торгов по инструменту""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{ParamName}', 'id': TransId, 'cmd': 'paramRequest', 't': ''}) + + def CancelParamRequest(self, ClassCode, SecCode, ParamName, TransId=0): # 2 + """Отмена заказа получения таблицы текущих торгов по инструменту""" + return self.ProcessRequest({'data': f'{ClassCode}|{SecCode}|{ParamName}', 'id': TransId, 'cmd': 'cancelParamRequest', 't': ''}) + + # Функции для заказа параметров Таблицы текущих торгов QuikSharp + + def ParamRequestBulk(self, ClassCodes, SecCodes, ParamNames, TransId=0): + """Заказ получения таблицы текущих торгов по инструментам""" + return self.ProcessRequest({'data': f'{ClassCodes}|{SecCodes}|{ParamNames}', 'id': TransId, 'cmd': 'paramRequestBulk', 't': ''}) + + def CancelParamRequestBulk(self, ClassCodes, SecCodes, ParamNames, TransId=0): + """Отмена заказа получения таблицы текущих торгов по инструментам""" + return self.ProcessRequest({'data': f'{ClassCodes}|{SecCodes}|{ParamNames}', 'id': TransId, 'cmd': 'cancelParamRequestBulk', 't': ''}) + + # 3.19 Функции для получения информации по единой денежной позиции + + def GetTrdAccByClientCode(self, FirmId, ClientCode, TransId=0): # 1 + """Торговый счет срочного рынка по коду клиента фондового рынка""" + return self.ProcessRequest({'data': f'{FirmId}|{ClientCode}', 'id': TransId, 'cmd': 'getTrdAccByClientCode', 't': ''}) + + def GetClientCodeByTrdAcc(self, FirmId, TradeAccountId, TransId=0): # 2 + """Код клиента фондового рынка с единой денежной позицией по торговому счету срочного рынка""" + return self.ProcessRequest({'data': f'{FirmId}|{TradeAccountId}', 'id': TransId, 'cmd': 'getClientCodeByTrdAcc', 't': ''}) + + def IsUcpClient(self, FirmId, Client, TransId=0): # 3 + """Имеет ли клиент единую денежную позицию""" + return self.ProcessRequest({'data': f'{FirmId}|{Client}', 'id': TransId, 'cmd': 'IsUcpClient', 't': ''}) + + # Выход и закрытие + + def CloseConnectionAndThread(self): + """Закрытие соединения для запросов и потока обработки функций обратного вызова""" + self.socketRequests.close() # Закрываем соединение для запросов + self.callbackThread.process = False # Поток обработки функций обратного вызова больше не нужен + + def __exit__(self, exc_type, exc_val, exc_tb): + """Выход из класса, например, с with""" + self.CloseConnectionAndThread() # Закрываем соединение для запросов и поток обработки функций обратного вызова + + def __del__(self): + self.CloseConnectionAndThread() # Закрываем соединение для запросов и поток обработки функций обратного вызова diff --git a/README.md b/README.md new file mode 100644 index 0000000..09b2edb --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# QuikPy +Библиотека-обертка, которая позволяет получить доступ к функционалу Quik на основе [Документации по языку LUA в QUIK](https://arqatech.com/ru/support/files/) из Python. В качестве коннектора используются lua-скрипты [проекта QUIKSharp](https://github.com/finsight/QUIKSharp). + +### Для чего нужна +С помощью этой библиотеки можно создавать автоматические торговые системы любой сложности на Python для Quik. Также библиотека может быть использована для написания дополнений на Python к системам Технического Анализа. Например, для тестирования и автоматической торговли в [BackTrader](https://www.backtrader.com/). + +### Установка коннектора. Метод 1. Из этого репозитория +1. Скопируйте папку **QUIK\lua** в папку установки Quik. В ней находятся скрипты LUA. +2. Скопируйте папку **QUIK\socket** в папку установки Quik. +3. Запустите Quik. Из меню **Сервисы** выберите **Lua скрипты**. Нажмите кнопку **Добавить**. Выберете скрипт **QuikSharp.lua** Нажмите кнопку **OK**. Выделите скрипт из списка. Нажмите кнопку **Запустить**. + +Скрипт должен запуститься без ошибок, в окне сообщений Quik выдать **QUIK# is waiting for client connection...** + +### Установка коннектора. Метод 2. Из оригинального [репозитория QuikSharp](https://github.com/finsight/QUIKSharp/tree/master/src/QuikSharp/lua) +1. В файле **config.json** замените строки "responseHostname": "127.0.0.1" на "responseHostname": "*" Иначе удаленный компьютер с Quik не будет отвечать на запросы. +2. В файле **qsfunctions.lua** замените функцию **qsfunctions.getFuturesHolding(msg)** на: + + --- (ichechet) Через getFuturesHolding позиции не приходили. Пришлось сделать обработку таблицы futures_client_holding + function qsfunctions.getFuturesHolding(msg) + if msg.data ~= "" then + local spl = split(msg.data, "|") + local firmId, accId, secCode, posType = spl[1], spl[2], spl[3], spl[4] + end + + local fchs = {} + for i = 0, getNumberOf("futures_client_holding") - 1 do + local fch = getItem("futures_client_holding", i) + if msg.data == "" or (fch.firmid == firmId and fch.trdaccid == accId and fch.sec_code == secCode and fch.type == posType*1) then + table.insert(fchs, fch) + end + end + msg.data = fchs + return msg + end + Иначе, фьючерсные позиции приходить не будут. +3. В файле **qsfunctions.lua** дополните функцию **qsfunctions.get_candles_from_data_source(msg)** + + --- Возвращаем все свечи по заданному инструменту и интервалу + --- (ichechet) Если исторические данные по тикеру не приходят, то QUIK блокируется. Чтобы это не происходило, вводим таймаут + function qsfunctions.get_candles_from_data_source(msg) + local ds, is_error = create_data_source(msg) + if not is_error then + --- Источник данных изначально приходит пустым. Нужно подождать пока он заполнится данными. Бесконечно ждать тоже нельзя. Вводим таймаут + local s = 0 --- Будем ждать 5 секунд, прежде чем вернем таймаут + repeat --- Ждем + sleep(100) --- 100 миллисекунд + s = s + 100 --- Запоминаем кол-во прошедших миллисекунд + until (ds:Size() > 0 or s > 5000) --- До тех пор, пока не придут данные или пока не наступит таймаут + + local count = tonumber(split(msg.data, "|")[4]) --- возвращаем последние count свечей. Если равен 0, то возвращаем все доступные свечи. + local class, sec, interval = get_candles_param(msg) + local candles = {} + local start_i = count == 0 and 1 or math.max(1, ds:Size() - count + 1) + for i = start_i, ds:Size() do + local candle = fetch_candle(ds, i) + candle.sec = sec + candle.class = class + candle.interval = interval + table.insert(candles, candle) + end + ds:Close() + msg.data = candles + end + return msg + end + Иначе, если исторические данные по тикеру Quik не возвращает, то он блокируется, дальнейшая работа невозможна. + +### Начало работы +В папке Examples находится хорошо документированный код примеров. С них лучше начать разбираться с библиотекой. + +1. **Connect.py** - Подключение, Singleton класс, проверка соединения, сервисные функции, пользователь обработчик событий. +2. **Accounts.py** - Список всех торговых счетов с лимитами, позициями, заявками и стоп заявками. Аналогично для заданного торгового счета. +3. **Ticker.py** - Информация о тикере, получение свечек. +4. **Stream.py** - Подписки на получение стакана, обезличенные сделки, новые свечки. +5. **Transactions.py** - Выставление новой лимитной/рыночной заявки, стоп заявки, отмена заявки. + +### Авторство и право использования +Автор данной библиотеки Чечет Игорь Александрович. Библиотека написана в рамках проекта [Финансовая Лаборатория](https://chechet.org/) и предоставляется бесплатно. При распространении ссылка на автора и проект обязательны. + +### Что дальше +Исправление ошибок, доработка и развитие библиотеки осуществляется как автором, так и сообществом проекта [Финансовая Лаборатория](https://chechet.org/). \ No newline at end of file