From 87425dfe9cd29b6a37b5b55c9c39ddaeffbb1a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=A7=D0=B5=D1=87=D0=B5?= =?UTF-8?q?=D1=82?= Date: Sun, 14 Feb 2021 15:47:51 +0500 Subject: [PATCH] Initial commit --- Examples/01 - Connect.py | 33 ++ Examples/02 - Accounts.py | 125 +++++ Examples/03 - Ticker.py | 38 ++ Examples/04 - Stream.py | 51 ++ Examples/05 - Transactions.py | 90 ++++ QUIK/lua/QuikSharp.lua | 140 +++++ QUIK/lua/Quik_2.lua | 15 + QUIK/lua/config.json | 18 + QUIK/lua/dkjson.lua | 714 ++++++++++++++++++++++++++ QUIK/lua/qscallbacks.lua | 235 +++++++++ QUIK/lua/qsfunctions.lua | 940 ++++++++++++++++++++++++++++++++++ QUIK/lua/qsutils.lua | 295 +++++++++++ QUIK/lua/socket.lua | 149 ++++++ QUIK/socket/core.dll | Bin 0 -> 177152 bytes QuikPy.py | 588 +++++++++++++++++++++ README.md | 81 +++ 16 files changed, 3512 insertions(+) create mode 100644 Examples/01 - Connect.py create mode 100644 Examples/02 - Accounts.py create mode 100644 Examples/03 - Ticker.py create mode 100644 Examples/04 - Stream.py create mode 100644 Examples/05 - Transactions.py create mode 100644 QUIK/lua/QuikSharp.lua create mode 100644 QUIK/lua/Quik_2.lua create mode 100644 QUIK/lua/config.json create mode 100644 QUIK/lua/dkjson.lua create mode 100644 QUIK/lua/qscallbacks.lua create mode 100644 QUIK/lua/qsfunctions.lua create mode 100644 QUIK/lua/qsutils.lua create mode 100644 QUIK/lua/socket.lua create mode 100644 QUIK/socket/core.dll create mode 100644 QuikPy.py create mode 100644 README.md 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 0000000000000000000000000000000000000000..5def8ef22761cc1191c0e73f7303eac25130dd90 GIT binary patch literal 177152 zcmdqKdwdi{);B(rWFTBZ55h>iprZs01Th-aV1iDQiS)okgMgqSpj>1X5n)D9Q4%Ln zChfSaw{>^Du&(avuJ=_0bTuJBf)NsM<)W+NZMVkJ6;L6d((m_M)iap{)_tD${r&Zu z&u6;3Zl_M2bL!NoQ&rs+SFCVkIvkEH{4LAjSdBY>K6(B5OSaSD=yUSgK8{y-6O-7{$l1_)|qT!Wx2yK>#pvOA-$iT zmW zF;#bDI4Y5|&AvPKWH@%SqWTO+)o}`hx@I~mvmK7zvoahL)1$jQ`(|f2$fwk=&FN^f z@k+&|Jq!=cUJ^oh&#N}dYFTp+S=R@m;FHjj}8_`zB@yKw`5&3-6FZJsab%)~tmfwwZX8Gfh`GzlC zv~VV~5GBx1=6m{R`DV|%5gAEK2YW(K-2Qw-zR^tm|KrbyxrTr&jqlh$BRVxNd$JMv z${R3u8s@>Exo+*wHG3@a#XN_@FyA%IR`CWNC94@|VBjaqvKZLOz+n|I%&B>WMr6Hr zdi-icr{IV3+!DUI58%b6NQ$&OLzN03YA+-p9nt1?JQ?OrF`R)mMi0eP&}=qho*&mc z@zlu3A{kjPvx~V1$eQiap}b;+!}r%q9N0eZq|1oG48wGV@6K=-MeB{IKd(|Zg<{x>C>)C61YqK-`~|yzhQo9q4KwWKzp^5kq_kAB1X!j3E2S2;JGSivSd~9bj`m*jFquvZX_(TRlu&VMSO*bpjnT?6Iocw_FB{}$=agFDlO5dvILoie1&!;Zz4J|01*Xt z#GepBTtC5mHI*)VgO9yI@+z{v@=8+jNO>`(xQs!px% zWmp(#ia8Sg#5{Kp-8nZH1;!I(PcJmg_l;O6Zw7l-Pe@YGY@y8N8%>Rpl1=E!Kv~rW zLCiKHo1Egu-$`y5jp)OL)2B)NCI7sTy6E^^BdP=PF5L3*&&D4$>t69NY>9nZ-%KhV zStx}a+Ha08MCp;^^R@daS{?EfG)KFMr+HADys4tLeo6Oz0c~h=;@E&$uSa$}jjbZk ztA3w>xP97`P2!bnN#k+Z$ZgbaLT;pNN_Y&jA@T_`8@(D*nc1^zkYD`dCu&nODWv($ zVu6GKNnH(vMpfmV-dWrwe!0+>7<4D;m+6yNF94y$A3<`XU^m2g#GSSn*W}GWieVlK znxBa8=1{2w^NQCJZZPJ$`Yop;V7}uwg~*n5UgD)@-vV@u*u*>^(!>ueWcs17QPPL8 zMtqN$wH%9?{Bl|VeWW&ogz}n^o&xScBzi7W< zP6n@_pS6>HjyZGYXeZYIE-Rdz>o@lY%`sl1WZlAZ17=SnHo2G*0H|JX{=8)!3+8an{lx4eJ03Rn#WM7Ywh8UrOCFI=TsFvbm3^BGZS zWHtf|cOaGpNgR_;^!orAprvp!x0l!M*J_T3gvY0WScX-fD&ljhN0!p=^f8qs(Uf=* z0;N}LwJ!quB<-dbn*RpLz%83zh6x6iG%h@gC_*nxE(YPc(2JxZZ92#WgJPpmu#?%1 zOi&C2VE@ich+eZ*WrlOQo0yr?Nnv2u*riP$$u`}LHtCVWhqc;Ta=7;E@Nw}PJV6*T z|K{wFnJZ=?RnqxIJW-04*!KnYeJt*J!3M*;N69brRaD~gNM5}cg{#+7;PvVHH2o@l zDhG1tMllSf_Gxat2cUDkjquxxa*Q<~Ab(ZplKX<%F!byC zU}if~cP{D=OfN^_lytv1lLVmfW3=lLENR#7Dnlbd=loznJNQsowBP(_&WcX*+PrZ1 z&&jJK{+3P>ThO40zcpZfV-zG*Pb=AAXro2Y+#qTs7MJ83&Ktpo&8*glB3RO#WWyH4 z(v3#uhY-R=JtRA>1*Y<01#CY>Z=ZxTCRe8gl5JJyqO*|xRoB=<83XSk!24&61PluS ze{A$mqCP#>AOzx-fJ`+?dCQ)pbP-0`59_5|5jYM(Z3~0A}m{^;I3t1Am z2#6mL$(XP0`xo~8Etwhk=mkv}isq_Lm$IDIAetI43)9ETZK?6H?I`1A8*tqwdz$za z>HyQ~waMFznaxIYoSQ>vYW&=KWJ6V94r3fd$J^!CW-&rSfVOTG9>k;L4k10g;@6{W z6AFXu9#~OX;)m&S+y&M87HRp&1BuI706U3=zeFPG?o)OXjg8bOtn3ZjVA~}IAnV#4 zn2*uadVm267y^ECo!Ex3zB+>uG0*)-V5SognS={#qzk+zI6IOc#6|=lc*oHCi+M7> zr}}9T?=wrX%2QaSn&7pdr)G+2>2=?(e8B$C)1LM3{4BjW-ntzk|@2?QpE=VLvXurmuMDVDecEA z;~yhNwZBl}#rAJST$27|p^d-zE8>ZAET@Dth0?<~+LCGk4D<4sZiy_^jMmT&X#qtF zuZ;Ib_D=Mcb8Q%vkwea#mq{fnKANf~4Krhvu)SNwiKq<1J5kLlI0e^NtG2;vB!Y?z ztuWYrjiR+5NDDla$Flip>5?*+BSOibv?h^9nw9ZQD5;)y;3=Qf`;VCy zW??ZKT#OchqRrx^ZkQsp1JR+R?+eI-`oFrEiEO_Zj2KmOYVHL?fB&u-FCZc|FLRDwr(^aC5%3ffT6iEA1yq_AQp@+ZUY4d8r~^ z5aS@}M$?acPFeuOVNR)Z(&3_#yehfcf_Trl9Ha|9pr1 zd&Sl%lETjiQWPG87(G^=5$^^_L=b(z;o|5aE<+NV>wA-x7_}d0H81la`fOf18Wb?s z8wYmMfXYVxfZ24y*O4#0h$=r}zh3=Mc7Trgpg92|>(5&Pv%s6Q8;NmYxw!>5O!WQi z9gjZebfEd6Gte<$-l&doD)E4%u4Lul-2;=;g!6idMj#k|SmGNr6;8C!h$(n) zdBy^?fdn?LqW1Gpe@Y7QX%&9Jllm#)Qll#*mmH(y{+-~f*OR5Nn>bZbo(GJN)nEBQ z7bvdf3sa0Plrg(D*5q+%0$5`fx7LA2%k%!TtRF&n%OEipHHueF@FH8R8&4oob-sRC z5)p8IA`G5CGeQCaN)k3u(iKXqyu%9ufAdSfO7k}d&3^=Rr_Y3 zJ=u2q*eP@!3i*}fTgm@nHm7wubMw{I%iu|&0RJlf~8+gHx^`IGHC@5rVd z`ZK+4O%w$~nzum55GRfy*Qe&qO|}%?u@-Sr=h(C+%kh%R#g@7L3%bbMIw%8Q6&RsI z*FktH(PHWa@S5W@kbvY3VI`Zft|f{wU4rnHUy>5O3lJ@PLEHt&VhfE}O{M%b^l zu7!=j+QVEl5DI~8=$K6?f1@{zKcV^mEB<_l_9ppr2G9@>!^{SMo~mH2vQ>{=n&i&` zkl_D~Kj)&l4*t9W$m#r<5&f_HIp2o^BKIl6T{!Xw^muS& z32#Y`%tSzP+@DeIKXPOw==2{sqUJ+j?sN%imzWMvwsqAamZ|`fYp~ACnsU(! z+u&&t0}zmEr^C$*pGbZ%mh&pIi!9O$bJUn?BZ6kua`fT+)XY+KT4{e97}?zPpya~P z)#Z!Fj9{RVrV#$|qth9`oF6 z2k($&mSw|F*U?h*d$EGqKz!YZT<8du*#1*BzP3oia~*T2+D76rMFuzO!>}4#2Dkl% zj_^A+8V8XlMai=f2$;)qS;JUagJ1eHm0_#kUnAjnryly^DYj9{^A@O{R8>zQ3(U)8 zRSl{tIhY&fJPuDGXf#Y8=)9O7zZu!nlp`LY8Fxxya~--a)${5RQ(?A64rPRLBZmg8 zJ`rJkbrr8#c|BBCVIrG)$&DDftRWSqbhie~ZKAyt{j6D>GM41IC$Ap4Xc9EAIo0b~ zT?f;B^yQPo#pclRBKS=<1*0%8=(#rc>e{MoaB3%dT|g_}8i@}L_d=ZE#ARJUGxB0^ zSW=;tw*}$vssiAE`L^X*`6PO2vvB%Ye>)wRr_f+t0=J4*^96EQo-ne&bp^^4>(l8) zrQFRR-H;3R+6&0ViAQ0{^;zL=M&tr3d^H#LZn%(2cM%+K@MDha4RZUH&!_W1fvdg_+!2k@a%>91|nHR1a|AG=JiB(Dnah!1jtuHQ2RJ9veReKv0;?(|Xl;OZm2E9rpb`S*umZQTZ@wu*mT z#CpoGs)70C1TUj5SK@qWs*cc!@RpTh4my0}tr?*XzWB(Otnz4S9vOnBsCLr7=99#- zJU{)O+(}X;omMu7Z2f1iU;ETzp zfDn~@ESd(I>Opgh0_;Tvpbi&H#b;Y3xa1mXO8BYoV0B{BtFm>~^AXU^cCrYg6Ju+$)dafgM9%Zkv=mQeB z7|UR>a~_vHy49Lkn2l9G?kH*4on=Hy3msuWaVnNgN_kc(Sjzutm^7=Q?^(?*JWipu zqbWZHPCYzvm5y2lQkku<_9D^pY<-N);6(3aaWxT(d7hU|fzKC8F6jw_{}M)ArP#?h ziadykIGZ2;SM^gkEzkJ0`ac11)IZ#=|B9p3Kjr_s`nv-GwEt0Q%R;ZVh^tlo&r1fN z{m?z>?FZq+y3s(j4k7i(lurD>wBS?AQwD63+<2F&FXr*u*aW*^ zLvD=y4=Cts$ay%22glFC)6dGsf5vB@<@xI)9r% z9#SYd-2(|YEuuWi^O1fD!(~pior}{w| zG$7SK6beT$qnG7b^}CM7U!?H3U#fPj8I&K{_`fmkD2<0i{9OJ2v43U*Ps?*w8WIht z0mSHIBXRywkSIPX66y6TrJ(3v*`;!2Zpu0Mn%{ObKA`G-Xrf+^-l7Z z-oF2_{-YeC|No|=omT^G(D(VrRXg7=>(oxA{}^`!li@(`!0#=7D4-3o=PY!sqU zo=KJAyRs3pF4(eiQuj4eY_cN7H$iRAq>;3f!>-z#Y0xw}q>Lb+ZpN`u5*;~KnMoU` zQ1k{6Y=a98TKcd*x?BdP@&lgI|ii0}F>P$Ot7V@gKFJycjDO?1F zj5e#xs73Te7TN-)+7$OZGC}Wf8dkVw0KizD+kQ>eCCjbuD7T9lW1fw6wRfgh8)HD4 zQ>b<<0x&&_QxT16sYvLD+QqAg!=A2TGzEQ$-av5!0E-Z!rTq<>`ppeA&iVrv{utN>np-a(lT8fyn$uZxjcGN8Z zdqz%8v1dkr{CHndh)ZD^;zU3Snwi+hho$+@sMLO|fVrunprOOxuC3}jT3eOnENBaN zH>?Iw@ax)bp`0M6W|2lW3Ws4smSKs$K0LI70_gb-^&;fZE3}>1$_E~J?M2*H@e4Fc zW|tdZtv1@zGuVz-*!al$1@<<+R4>)nctI*#zdR0!NTsL;)qm~uWk*ZriYxl_z_^ZG zxai+GDtu!6EER@Bn&~ISb(T&WpSj+H@ChotPy7@Wmhg(|@=>yJvQ~mHI3CbDbj;#xkJC*BjdM@tx08!pVdpRYmq{?}a z8NgF^K(R7C_rDOZ8PHoHKn6UASjkVNW111PdvztZtN+t z#zRaRJB^18Dx4Y*Us7SdPQkMG{OxK?#+9PsJ-U29ZXkgEYfNV5vAZ;q5x4Ah|XhUvm zj!nuEwWtrfRPYGp!L1*aelCnb?P~e$;<;kUAk2}Vy}m<$_Y-B2dKu|AcZ+}6@mZbX zg;;OLe}%jL7TZLT*5yK(ZtfRT5zliD|4K7FVllGw{fx zFfGDsm;Dh?=d56i0lS!v=SaJy)jWad$YI}7>g(>%a>?CQ;$lL;EJZiBz`{7zUSEJk zF>>kpDWPswL&@6<_m;dJc1I5R7WG1W6!8{xIhGb8KUpD{QS!~AOvC&}Zn(G9&d647 z7;K9COKrGsO51Rcvl*x`HN_Fu*iU&MH;3Gs7uZNAz^@IK;FXkW`fQnd3B z;Fg?jh)99XBnU}lKmm32*kq?TbgrUskQ7D)lFDOavc!+bOcH;mWK+y%VU;d^L>W9E zCX#FI@)&a@=Rb!$rD*)8G#dW_gHLJ?-Ipk3J|JaCk@*CKJ0D-xci2Je3J+<|BWNMQw_Pi(VMbCqoTCC-_iud}V5(dNv@`=0Bu#N<$ zH#&wF8*I2{O&UleO{^+lCwwEX3P5Y?mz`I;e_0W@0 zL+CW37h0rZPeE+JJdC~Zq1)2VJEV+2=7eQF8AONdVHb;>jZFvC&hnkhWgBeb+ZyK? zlwQs9mwG-Y@^2A8;>pJ09viK_Ag@aN7oKdZ=pLylIYYP)*aTyb z$h}|z^GaX9ydGPyKTB@>xAiZ_Ppp#Kr(tM!LWKs*UFD|m%LSbB*d0!>w1_PW_v28! zfab8;%Zx=DX%;WOhXX^a*o$nEe66AikGAx+iczw-Rx^g2#oFu)x`^k>^-30-!e~sm zq`uop@r`-qX_V^U8{qmVAYA?C7nc z438<|Ify8_?lvGrGi@#nwizi<@0~^Bfi2u}--$EQl1-aW#5}LD3u8@Aoe~r4QB)o9 z6I=194#WNmPY~i;?F81Y2un)<1jg!ClDL&fQ-%Ji3YEjuP!S&>0wRyIAq@-9q<)@S zXP+!-kY`F7n9}u32|NKijm({?rX7OAMjk`uL<~*i`oK}_(L(b}xlO%jo3wOx$)Q8^ z1qSRmBXI`;=e!$b+gq?HR0bRVUu~jj>UGln_jjye&|ex zL;BCW=n#Wq#DAC22+IdPuyMN?P={q(L;(WG*i&X~5&uMKqiJ1osC_j(s{;WN3wvJ}PfLX*f6~s^(U?OH$(g;dPMu;K z@L~|#Gk42oY(aE}cwI(sVl-8NEP#~p@B*k5*gu-&;TCMwL_(Ih7YT@cbU8+BQ*eVO@&d@LL6UfX8;nsVc5$}o z|GC9cFX!32v3DT${0tP<^0#tz`F%JvkOj+!8v?P*vc=ksIOo$u3&z5XD0Tuq1=y!{F{0w_+NgU5vvYaHnAocwg@fJ>-+7|vJYR^&i zdU888I&_>9PYO`i{%h~gnV~B<@WTLh5X+Z;Xuuh)qI&GA1;?a>{05k)7{e5Z$rN`Z z2CWao9+Ui`>oiGkj3*?5k6j*jSjkBuj1c*7(MXa9%`K7`DS~bwdR|hZKj@yq=`9mN zpw8~UgAT>V+7Chl#1F$HnGhCB@R&9Mnq3RyYT<|0@SpUDk1 zT(3^PpCDD6j@r0@dkWU3X?Rs5oOB5Ckm;iUiz#*+yX~VVL&xmIm;uFgy0{)Gh!m$0 zEu^=KPhfc2mI1d=pnFemOjXnloh*I;ohj<zoUL;tar`N-9(gktLGM!(Hj46o?G~ zL9O)tTNI=gD3p`Y^2ln6?|=|D`MtnOt{@tu#1RU{mIr&kLWdZXO2B)?i2$s00jzZ? z2KND0GG?a~gDS+pF$>mDMHQ5Yq!iHViv60%s6MCOu%LRLnSp*VLdt>cbSNWxr+DTA z)hQqxpz2`Z0eHJJ+&46hoC`P_FF}iDC@biu2wM7Yn~s|Qd+1W~bFhR@5%Zf> zq@Tj--tDTC-Iycmd(|%gg{<$0^3VUhYx$$d|9A-l@(&o$(qCSXp~N3r`Z}2;w?-Vm z;;zhM_# zDhmoDwwx+RY{t~xe2@gAAK&1)e+(Hr;MeD56CH$>+!_Z)?J5!#gO5*K2z>pqF?Ro% zAOY_YGr@t5VQY&RgjqIcT!y&iRJM=MV>url&zf9+JdwbIm!+VDY zk1LS|jFQjc|LiJXUCMjx^5v=WE$QX|{l$MN{~oj(<;5lOhrU}O2}rXkeim$yuKn*R zJO6ty|D=9Q%da0Lf7s6dl**5JY_^=o{u(O16J3%NK1%;N%Z70^U?jZ(kUsGtV6dNT zy#uDWGR=9sJ86oqcBo$!9fTYJb8c^bGB zQ%Sikb_=}rPVqb0muZlNj%VLKkjFj%Q)#mZ;ZfNDIkcR%qAS{?Kqm{IN0Vhgp%YZe zC$JuLKIM29BwFV<5&}UHn10$ zy>uBOrkD@)`?Sk*SnobRaq*lk-h692fxVK5aTm zxLqFr3MY*>XA|ag5~kbrs=Qy-0DB0hS*$Q8nNS5CyTT>DJ^;Y9e3v!^XC2_q0-W}n zm*RR$_RakWeB*66RhW-Nl$Fc*NS{h=TWNXLE|x8vNHZJg@O>p{US+Cz$J@<2Ro>Y= zcNuV;nsbVbmcnqU^6br$8mNT)PEtcw0`9{bba_q>RgbH$st0vX&GE?RzBryvRcXU! zD?0QSTHaH=FGf6biqup%$PR<_5d_q54lkFqL((^(rpgzhyyx&O>;i|R){{N~>gpQ+ zKzbxym#9%j9%9GD@?s?TwN(zis{NYK>3Wq|sPC`;DReCQ-5JQ4T8;VKi@lG%K$XSW zi7edCLsdBuSyhYp35i6{CndRK_MY^wL7r95I<*)_7>1zAP7<2J-@6v$Dj@9=XChBZ z(+wkoU^hU2k4GnE&Bw9k*d^JVk2ZC;@Dmpu=J1j<+MC7zrz1%vg+`Jl-mQ0XO0VYc z`+FtB5+Ai?1*fK+1QYZ8TGA~pe)(4_4^AOrEKgw_5*&0N-FLAi+2VW5N5C{rQ`Y^2 z%n-mCa(f(ABu^SqF-|#_lNKn@^Qkpgp-AM1!{Hv-89N6RCAz~77!12cPGL5SlaOxP z7qmoZUvR%YAWwl1LJzBzi)OA|G|Tr2#XGxoH;c<9R^7jy~7qae!n~v8eQ-d~E0IHZlPl3|24Nz=2Y+4{BdVU>OQ@*(p9j1o{fb zpm{(z`IZYgAqm=>ZhUe|G8HfF3C0#f>RT{1@q@+&MILt__=P<=-9zYnB-~gM?snVM zE5y;R!IO?WI)v@>Ab9F%*P(@M5cW537A*r|zZC2o;B;^YK~hbhf>=^Q!F4JHa?i_~X4 zgKlnx&J^FE0AS8T-`jI(*Lq^Q}i!HN{ap*|0VsOSM)y# zR7j`)P&9?~e+bh2Urc8ViNiqugULoo`k#bGk^VRk@QCy>kOl15RSqU>77w;7R-8=Q zKS|{m^IR>F+9C!b$SzdsWHbtq8->zwMdlcct5nR9y@MQ@q>)20BjZ4Waov&Vrv5uK<0mR*3|MWyb(%>%!eMjQnbU^=4em|7+}#kk5nG1h&D(k zYf2H_G@sry=!z|3G)t#*{}bpRB_iV~A_hgIK$1h9KxLsE6yjFrv_q(uqL@5{x;#JPBbuhui z4Y^4P?qcguNV~wBZNq!@S;ABP5+AKl%qLI5E{Cr9bCUHRCI5JMW4q>e+4(2Q{J^&o zX9@|tX1|*0%JGl$BYC8IMyjE&EKxn;k-v2Hza4|-(V~HS%vAn22V#!&zj5fIVvSAq z{x6(wFylc8crN??rNQ)CAP=868R_T_ELluH7 ziSC%8PFjEo2Ioi8H7~u?l0I}%OaHWak(cgBfVfDyB!eb=(e%^DF7wLnBkr^*qb8oM z;tr{4L77(Z1BOG@Yc7C9#$lo-jxjIuR+t;b)VD!T>=Q-W!)ZOtOqO)1LR5^{_9n3zFtNxPkP zf0y#J?eedpyz=K1oT28nQc9;C;VY_hm&%{=&m4eG?bObs#L508&t#>#)=f5yhNnCGXGVcmYKhR&B#bkuyRyEtcn*d1f8!rXUNpr9VN4%4WpaikMgOcFQ}?*7m3{4+czu z{f-9C9EP*;4y|}^bT=NHU5-o@W|KDLmAnNq|9jeyH`Kl)&%M`T2!_8nf*oeH4d|X= z=&0XAo8cI(<=H*Q=@?Z6Pi&~ysB+x2+LbCxx1tZ^q1A3$%?896W_Rjos3}?iK6Z0) z&%Z6YS2ivj%w3=~Hw%$oh>p@sM<#p$@H<@I#6;>l&MjaudnNH;j&MigOQ;@*g?n3` z%kYSm&%6X*$TSJNhtVLze9!W1MnbXyHDe)sMfF;KvZ1Q&+;60I=G>Df35Q|-4iFnh z$Vu6x*)Cm*iFHYd>gW#~8q|JLuY5H!UIe>WI`u*JqWx>{n3Gk7Vbj35W#VG?!H!o< zuPo0{iSsQubpP;dSW0{3W=fDqmH?d$(#i59kSJvv+cu=s!z<{c=y&K^;zWO#<(Y=e zN%hj<+mh*=Z&1JZFl1rh4v>?g&wu5f$gVz8|tcvpa>Jfq6d&lTC9>Opf7=$I4C!p4@#R! zSa{XlSBBqgsNED+PH>}9uWr8`onqOf(3nU(J9KV!{3LdjQzP+XLwzF$Gs73dkui{y z?Z|q(YB}awb5qB&)T7CG>Qwvrr{r^W{2*2kL$h$sH#7kZ35`x!*U;Z!woksP1#cfa zyS%HVEqfCDfHMJ?!idaC_zzMK*>xREd!Cf6H%W0tFTD0#y?D{`{6JQM&gg(gkoh&F zB@5d3B)#9aJZpZY7dvRpPjI`oM&0?hX*e(0-v zi_krLC@q4HZ+Skrnq2E+zH51!@rb?r&>)xLiR*?qrjJrd4I(wI<#|SyfP$q?OpTje z_`&yca6m9!SDc<2N9qx?209Rw#!-Cum1KqC0orTmrKK6+{5cIGyQV%g%<_CbL%P&4 zbz&h-ozCZ=jMs1=*jVZe|ATpP|GUypX&L*bWlUjZ(<8b5lX^c3j*UZqMUTUP2D-@O zS6@<*g|5aBVR@bd8rV2GjxR=-5vs5}rHD{tRL-c2(Eq|0l?;C8OqXWm>(JLT(NO zzG?Q;N{mI2`)l%2az8`Hypgv@-uDKYAj340C~_4%@h3%2nNb5M?l7YsMqUt>W)ycX z(}v<%1j~~FJg~_5%T|D(`E(6tpwfK${Z%&CwA!^Qt$T;v^f6)#vm0$`>~xz#!wk(b z@PZNAD8wgaO6gzxuS_Y9vp%v5K4b;0XdAoEH9*HUr@G@29pZFpPECPYLBq=JZc!xD zw0ld@8SJh>i;8WgUzlW|Ma8z$ks?V6Z7>Xa6WU-UqK|4(Nq)#T?L!kiEepHaa5T=g zXbFZLgD(e!QZ(@B#$$yUPca8<>CuTZew#C_Q;PhP5<5RS6<>(xM}NvsvWxw}+rhJo zAZ6s)E}Li1W2z;2_Vtx1o^3dSXW#wLc^1Q&n*W9Q`=5CBJkYUuHUW>|+1Zk3Hz=OT zCMw3s23npQWG?OAvu*CNrzhD*ZA12vjilMp*f_hpWFr|wHtJV#jYXNptJAcBz20c{ z;CNJ=9FK}AwwC8O-~c^xq!);Re6PCXuNz*{Yo zI8B6hwif}-t+XKSf@vA6PB#y}yE|nbELoz=gQ>VD&4Wqy@ITwWhwXcJ{f*AhNrve(Vy~!KkF90W6wLm{3_HuSsv9HA zTreJ4*d58t{f*s1SWa^WW848G6%>>*Qvz`EJdItWQpkr;+XrJyk2=S5jlWK+d^yjn zFt0dql73~UhVij}n|ddQIr(v6WC zXB<~^uF_sZD;=SdNf9>fzESOH+&`MK38jZV>p5v znP;+Jt;K-0swQLZQn<|Tx$~XdkdXPdv@CDM6oH~~g?+EH@AujFC+z#n_I;au|I)r^ zVB*8_$J_U_?fYo^euaI%&c5Gj-|w^UPuTah_Pq|~KH;si!|&VoPwo3Y`+kfq+MlAE zGC$|3yx(e|IweRcfe5393EA04d?E6Cdey4qZ!oIJw@1NTD_hITcR>u!Y&grAM`ySJ;zvtKi zxyK!U!ilF1Jn6L4PWGO5N=MdCPp1ybv%`a}!Fwh<9oFEnm+|@!`MgD5>*e)jd3{n| zAClMQ@_MVh&Xd<^@>(gcqvdskyq+enx$@dmUVoIe{99gk%j-6IT`#YzSJ>jHVbMqVe%>qYWfEU!c5^+b8?Bd-p5-8YHt`dnVO%WJc|u9eqk<@GUnT_LY` z%4JH+|U^Gb^+3qUFr3c6;mS>)8F#!N!YIkvdR&HMU-%fKp}{@p0C!)9Gxp)lSexAO z?oel4^py&xm5+RdcaHVX3&POLt|1M6Esm8_A?iP-1|7cA&qdXX=LO}XDFW+oM&`~da4N>2XwIz z=0UCHt&U}4mTXijBV>X^@tnh`R zH|xU#%nv3~|TnWais=@bva2M)_HJ&x}|3vhTX((YVb z4vVCQP94~zJ?TLscM|qz=ddeRw?c9j&d zoZ(^s9f4v++gPkG(o%t`fPt!esH#JKk_p)NaVd;r9A%DJnPFe5&S7bFhK8VID?KHE z-y=4~2_6(C`X}>Ro<9JD!Xg3pHdB@BYzg?`!#hv0^3@^F(uWf;+xc0uz33!imv|j4 zz~Z|+ER!Y#v{2)}gl7SKPx{1m(e5d`1i0|M=%ev~_nOFH^V%L+4)!bWmGEA=8HX1V zp@)7|EMLbXhK}MaAk~I#gl5=9Q!GWF55N@ao7cc z%@qA%G3UGhUx~Pwd+DIc6yN2xe;+&^@V8ZvbFa4(7iir|%;B+YVR@pA2i9jBky-e< zhkbvyeeGE zaJd)7+<57u*C@hsQMubFYT#~V{s4Ut0!1BF8JZtX9@k69kfC(&9B4!Ew+y0ZU_MGl zf>7Yp_Y`((@SEf07$3{|0^#^0^c`a?69=MXVRoLFwo*Q1*!*PLy+7snKigj z%9W9gjZr1dd|&AvLwhObMF0!``)u~d{KglZi39c+DfKUVp;!g=lsHaYk7i(~@X3)U zly!D}qPzGE&bt?#vM6!EfQPQj0q;QtFA1v*$kx<{S?Cz?^8GTI}w}k9zZEn6Z58U z<;B)k^EhO2nkQA3_b!`;izSc{Uf7=VBD)r75uf=h;Bs1>gzgefJfycX08C}JXE8V_ zou{pGU5~=Gn_egDG=JlA*Ods1o3S599EWT+1^SCe?vsHYpx1cBC%&xXSBC60QGR38 zTQ$myq7HLc_*yPhT_@fH6TpppcoXx0WlsYB3Dif^iS;oIe=WnI;h=KS9PLKehtfyU zY+*-=YZnzq)b%ND4Dya(0=JE>S8;VpiUWl;AcaF5!bRFjFbNt~S6qR&b&Pby7F$+$ zi7_*$9uF`$Zs#rY?dp;I{&sl!>R!Nj&PY8sJTX|XPJ2zq8&vU~&CE7D;+%yHW8nvbG9{&d(ro=>6U6e$%?sz&WVjH7N>D63{$xVN|hOoa{c z0c?n5gDGP%*Iw+TXp9X0gt_JWt+OW?v1jvq+_Q;~t$--zE~U2bIajLfEVE_v5cxhp zu>m~;XvZq{=&RenCov0XF$jMo7C;U2QNgYkdvGTbF&s)o>SGEpFHI`8*lw9BGj%98 zOc`T>2~?+q$pr#~Em;Eg%M4gG zm9X7?xWmk&S8D=b!ua8c19%)jW956a2S59zPw3VAeldS`Aexm|-s^3^$ubXu^4WOfa_?}Z5y{bVGBBgtbD^b; zs_%w359N=V(JjLfI%U)X-i~*@c<@Kqk>165#u_IEWzyHs{?M?fzu$CKotWX!Bh5~K z)D^~KqC4W|;bAo`Xn)N3p=8feEDmT-pmx7WJ1r|&x3>1P9*cUG_i8}dktovSx~;bw z^!?GCC3w`<{;R(hftbTLoHG*6xEM(%_{)j57yHxz^`Ne zH(7qp8(z3?{~RzIlP4iM(t|kVvT9pkjnn=GwHMwFMB3fjUHgF9oPahT=e=gs2ddkv z5MJz#U(e`{^7~!iAJ56gueH9;U?%2t4Q?ay$(~>VUi97^XozB z8{*W4=x(FBp-P7Il?c8Vu&`jF*W=%Y|L!@s>H zg}RN#emEy~O*Fy<Az{s1Fz z|28`Q2R>^2UM#}%C|6!!L`JWT5%wAsEGnp>Kj5iREd*`t_?M*8_jMZ}g4v>NG z;Y;C%R8^SQW>=W!1&N8rp{A6SvN#!hb(Ylaee6}$&9H+JZ3cgEs|{^|%y$4t;7wid4UfSj zY=b%R2kaljCrK;dZvQ*=B(@Naf$5GefPF7IfL?~&*2efb93|@G8-O3aWr5eW^a>RG zsN?gZk>?PFZ64-!9X534HoQ?qHIYi2uUA167cKLfS7pI(;|=-o<%e?U%@NofVq|VI z{ViEWN&CWqcz^*uZ^~3%uAM`V->SxgP)GWS&Kam?YQ0Lc4qet4YgCGzo8}ZMe83M2o z5!=~DYz4;vFll1ib|-AT4x^EAHJidc#a{qM9L6Qho}^uTe@7I)$q>4BIo_j>LPqsS zDH#(Yx8!Dp;YstJ;dR}A9KMaW81jqng`*wk8E}-rfz;Q?91mMEkS~UUO$l$}r2P(e z=s0A{$BUQ=6`SEPOY~vBFszJY6gJ2}yBXG}QpzfD)eR+Atd6@h+5uuvJRdg$AWr`%!5Flo;-X-HZ|=`{wTQ4X2xjV7>=B z5X-(Wer6pD<%~IjOI!@EF)+n^Fg1@ibI!qt#@z`;WLh8bK1L}s$BRh#knjb!W3tH* zYzGFU<0Yh>@OWeEpHl%-b}j1@*b69y!6oYu4?x1%Hd}Al`?BE!TIa&x%1uadn6$7< z?!N%NFy}eM;>^(%sNaaZje|;T?X_fz3mYyISD85`a)>*Tj<}20R-tTcY_;%2iSUg` zqtgpX@Oia~D^)g_A&pICOVqZAj|eBn%qc@&w5*slp&`Y$06;6gKLRxP6>MptNcWdC zQxq*@InMxy7>3~%Vv({knr5k()5d4Iii}h)8{Q?oJ4_0k=0pdCA*_VW4=4{MT z@|3J612V?Cn4T1fx5T@W6S`GYCj>JmMoiSQ- z2Q%iSaFvxd{VXsy}G2^_&yMIE3%O-_4Fh`EHGXYbvn40aiinZP&MIEIy^-SZ-u@)Nfba zNPJLE6=D5JIIm%@B;lZ51Avn4O-lz3m-b1^Ldr%`if^|kF9SZ*5Zko;yEIE<$u zqHBG7br?HZ4F?=rFEDz}%E)lsJRrUfF&*@$d7}*p$nxM*-!x083h^s6x4$o#80>4? zL@N>bA|vY=N$aQ{F>Q?5&6qvm=LnL9jF#V!=$*tX5E1UA?Q~+AeWUf&W!*_5>Kkc9 z<&2GGIRVZMu`%%1)9fjlOTag`rmq9yM)<5Zg?E8oh4e0XOS7vJ-YD*4acPBh!v9g& zVew-0Woa+L)MDHA7?HS}ZP?$Sh46hX7?z|t(olWhY|N-D3L29Z=3H%vGm!(9!rlTg z+}e;H5(e?yZB)mr6i*g_a5I=t$poLWIV%zAV`L#Cy(nIY7%yYon!9)3@biRdlQ&7w zTk#cjs7Z{Y#niv@{ZQvwo;whVv`-EX^~cJ(^EE|ypVH;9w}wZg{^v{A zWogUv-FdPcmM*Y_G-~4SeQALH3CBh!II{@{a!)wp6`V^6=W8xGbcQoF4bJlt zj+7ZScjrFPoI;eu-m{qJbV7wyN2vV>?lHOI@h%t$%&=m>;kK5n}q`;?*w9$Nq2?L@*jK&Ewt4&;w8>D~^YS@OysBOh!Fk_`rC1L+h;O)cr+X3%{ z&Lun7-=&tfP$aDu`Myrtcqx2?#Mh6YfyoAujlj*a=4mjh~>0f*DS8FXhE!RYMeoR!Um2W$%Kik4R) zZiPQO`(e~FdnNua`j^jsB^Z5*Cn#~Nmp8t|8S~mD{^(O}h!Bsn@JuKn9Q@JuH#zZb zH++&VoBFK=Eu{0K&?spN=huF)Bp)8Fn+GLstU%`3Z0-PTb$0oepO)y8%om#DU!K>^ zFk`Gu91rr?HT#Qaohh&sq?JFuaD5>kSVVrH@_j%GWMU6wwpC^J52Q^2T$U|?>ZHxc5K)& z#>khlQCql&QT>wx4>$KPN}l1v;w;#l=NaZ^@!Kj=KpNQt4fB`GE-qqR^l?7>RymF6 z3oHWH_i)7f>XO!UC(k?v9jQNpvGG%I9wlJj$^Gt6qq%dMgU z%eKMHdVHk}GcYW#E^}d48#>c!F4`8=i*>W{z_(TF@Ypc_;<+5V_rX;?aDY-;Q?t7iZKY?grOL5%Q?m2_07`OQ0RRDw8- z0#quZqYd30Epe#;E@u%JJvth5#IA2B|G>u4HJ{>(b>kx(e7~ns#|j%As6QT3Ahd3w#uV@6J9+w z8WVg>N;3`1=jP4K_AMVN!|*`#)gy;+zC^bg%S(1*;f#df0>3-W21x4OjXIXesBCY- zTz6Z@1=Bs@j*ME<6RZj6;4L!`A98Y~pwwB#O92s3~U^e3*_bAgHERa3^T(pS8(+ZPyV{>I%iFN zsDE8s-8%jJt)X7{G)bMat_}W#f8!BJ=Oua!D}VZ94Dw7t{9f2(hPA2ASy~u6pr4OT zapNrmAJNq7u<*7nVAU_rV6G$`;O&~H-gzKuc( zFEYsSCdXsLyd;k1hSRLDUlBm@P&mo~IMNC=zsrcMnF|?q zXf^Lr_rk^D2oDesV*Y5W=5w+AuUAw2DWqD}3y?LkhF8qae#ru{GoGu2!8NlI|6z_e zc0qtPWx0{t@*UbeS0Ge3PB$kEi|M;duUOnydtMmYn+@8kVE*pNCmKwzO|h)E&1QTq zCci;sMazao+OxG9EP`OJu1v&;aB4L>fT4Wg?g>1E=JIj{FZsM2i{%Ne_7ql*iTMkV z5b05404q3-I2_TAs@wFT^lMDO7~CSpLHosjk7y%O=7l_$c}=4o`)G~U=(ys7dX_r3In>rDueC zmKKJ~kS9Aaia8VK;pznRafRMQ0j?Q|!I7CGK)OQwOC1}_e2(aC#T&~eIucOvEn*Td zfMkvXez|qo%g5<8o3z?TX!?Q1Rz$Mnb;XWaIxEbwe6k-coM*3QHi}I-DPCR&*NfSd z_(#&7!>SMt{?1aBK&hA%1wWbr?FWUSyTg5}AP^y!RhFN~i;T;+5+~siDiGaMW;V>e z{^jUqDqvx~5KFm}J8Eo)k3{-0;O?QF64c%d=;&4(^@*_aH|cO7Ej>1Xjb?@k*5Q+4 zy^ZQm992-{5DoaD<^{~P&%0b;uJ8eR9?*2aehVxEY#xz z@eHe8UvB6D?D~Ndu)la2qnIt?6C{#}4BMMRJnMA5^rG-_`<>ZZ?PUBZsGWg^T*CemLcrgwmJNRaV?NR8Z5>Ii>U8qjK9M5fZ9R`WRT6SbOqd7m4a zS9)D&YU#Yth|*9fuXHg4=T@!u6QniirEuXm;_sY2#I|Aq?)lUr3hFWd+1 zGopI#Y7h(i)a2V9qIzkZ9R_f?CKHdP^3Uza4-6fN_teq(EyxMpITCa5z$PWGGR*P0 zbs42a;Q>{*I?oQH|BTK+Vo!oXb&1l_b7j(qR8p^G(ka8HlzJDRfDT`8taZBY^s*|f zcCA~~9T4b>O1ZD(c&c=hI%8wMi>MHMfQYJ3A^27;@x3}lU!|AMT6URM^9BIu=VKzd z5f7zhp)*ST+P#f@9;4m+GVc>YlYQrJ2whuxMd-xRtF;=g2b9j#Y9GTLe!Qh1Rh&E! z{{&|78m3-*$8Dv*2+b;;AG*Bs#?X1C3qmK9E(*Cz!=Z`3^EZaCT?<9*NK}ZGU_fG0 zlU~d)TEj+{*EOl+v&=xD+0Y04h|s}(cD?)rK-FzNhgLfjk>-TLqI!NCsItgfjxPk6>!2 zR-S=Z6xZwsb?0+IqK~<5t6*9-!`^^tK^xaK_=awH`s1+1k`hH1Fi&KbpIu+qpby>n z^vBu*_2s}K3!3MIjsDWgLf`qDVKkh-C7kK2``))aR65>>G-a5J@aov>^9vSL24jC^ z$16Cne_ih|Mm;QiYgVgslax!vJdI?xYl=nY?Hl%u7cYTJ*AkzNNQfaiOm0%ve)Jr|u; z9_*r9(unBP_PRG{`r7#!voRIe$cp($=mK+mY2?d{ke9nZ2SmQ?5jqAvHz(wV2o9V1 z^f%gr>k{2wc4D`|Hhl2b(6!ebHQPeDP-M=nLVgyuKh~>Pv%_M6%%FmUwfLe_*^2Uf zawS;~ah9g&EyohDp7$~KD%MAKAB@!Jz$8ta7(Tx07X4ZtG{t>PxHn`B{I_@P#%`J7 zx(Y%0qL)^25x(OKF9=3Bu)vCQVz{p1yG z7H*2^tt0>@%9KRU6ek3H(nh-x8&@e--Yh*|!(m-G6QF(Bl%QEfL4f=RHGjPgbey+t ze7nBf-BiW_4wl`xDa80!N@TV{WVRa72@n}>NwTLJsr^^BzQ+0`EZ$vapmZ(ZJM=o? zkKLLTIp|t8JCut~7F1`t(M}v3cZNn`7gKmD)RO#;JPo}kSTilU+i}eZUBR`S3q>~! zU-nr2r<-U{#Pfk7-g21O_!dObk$CGZqMTSAiFXUVNXUO7LI0TeZIPAu19%#A-sXkE zd>ZWm(aZ(yiOajjhPoLu5wA0`^oYdnhV~W`25GP1I`C-8Zz3T}TeYCwR%xg#)CcZ> zTY4A=K8kGeVF;jT-`=CCOm^r*KV#;E;>aflkU(0CTd+l)K8q~K&&Mw}6?Ew)9N;ZZ?C zC(+Ctr`29%M)zxJ7#BoL^*_kR2| za~}Kbz4qE`uf1M-ZD!%C);%N_A(z{$)@PIjh`804Rz`*Is>u=4$*rBZwu)$5yV0s)g`rEViB6ZMkTnn0Xg(|BN%?mB$L`%*}Ob~cCOf<{ZAa87chii9JOi^0;QKz|vm z{3Hgs*Nt$}5j;!%9m(HZdgFdQRBy}{=XsxGAx?CD25Zku9XE%I{OA?q8U{B{^+=!^ zldDX6Nc6AT8<(D6Va4%ZlkRXTeT9w#&Op`|z6@#R+M?$uj{RK!2(==Y8=$ev27U0I_a>*8X``c zthBKVc3HdRsvM?J@H?hngHr!Z!@Scd=WNH|-OT90vUpBFS1eJVS}~r-7OpBPMeFl_b^n zk0Y8zmUQ*4HJb8*U~EoeV{&hXQWYx$+w-#gj7GIOkvHuCdzTTpjc(2(Qg(5%imzwU zJ>Z|+sWyhoc=S*)bVp^gIlGS^X_CWNf%sM}_QO^K;V6^~xth>dsw9&&cXcw-)yyMM4W z)#!J&g*(rl|8Y$JAlx}*QJ+;J46j`g_d+YH#84SNI9S&%ZCtCH-gsU{?@#aTJZSwH zpLvG`->CRF z@!c!-d6?T!Vg=PHvu#~T8BdhO_H~hyu*j|toncu~Mo zavQdb?r`4!?(yM#8HpziXK3!fhSS#*&a;r}{{@^E9UD$uwce7V?gzk#F;$MzrDsE# z17aHM6R5naD0Grl+6Cw)tUfB%FCnoMhB=oIULTfGW^v_RF4iY^m1_}OL#N<2pW+G* z^h7yu4Bj=RAnDYjDg8NN-X3k`7LHKzG%PK-jYJThXouyQuGas{87vH%e9)spVpB+n z!-R9|-RjisI;A#h$y8{4R->5A->6phPtEhx9zGjwp*m)|7%08L5$waG(vbUq(w2Zs z#j#7DK?SL6Mv{(Q`d7L9BbUDDN7P)V)&D^aRaJpM<~b5KU$;%2HhL~sD!*W-_Tb`R ze(k}Tp^F77{ci}5{}vMbrj?waN4MC0YCQsfCmoL09&{|k#+pW>gEK!;l+p_b$*PXc zcA;2Rn_ud&`RGxHLvCF-X}6KMGBdO|8@omV`>j2?dVzeEpWMiHEj_vpZDCvZtv=zV zex7KH=d9*Q{T8`A+TUABo0DT=Q-3*W#G-z38@8@Vo)DY*Lf8F8Nhcl_chjP&08s5d z80@1r^!3Y$P2eQeBsdA*<$~c?6{Jb+n3PcuD9Kv*FXyCrk>YQ|Y-^iY9iE95T78}p z&boDRL8gnxtP%n~%;v3X_m`o|rN90(mh{vHAhR1$bepZ}wC>m%)(uzk&CG>dv+=rP9%=(vvx=8{M{`+U*K5S@aZ^FDhNY|0p&@^$AGINRN!Y zsIvRi8K0dxdbGLrpf5PA_Fzr0f9=6r<JW2DfnrtiHXxtlpt@dE4Ow+SNp58weEM-3)ko{PA-)3q^_7gCo&A#Xx zzUZs92g^bp{jWV|S{4qtaaUC*WUHbU`OcMGPd(HY)7xZvX@B1u-domBa{OPtO@x2dnL2&-bkIp!_OhjeY7bT}R}|XV)(why zxbcrY=(lmbn&RQk{tNzxt=_Mj@s6E(?H6b3nlJ7(TYYSgSZenm&G)i6JRJuO6dgPT z7>`TB`^8&XRpf0bOPnIHh#BUat?78UP^*j=$2mMk@OUH?$Kk&460L3%PH8^}%Mb|S zttbl(AUnC={bFbzH`oN?Tx)fYQAUR9;x$ho9D3awmY!`h+V$Z*&Qx3{eBA3?FC|Wv zmj%AkFMW{#zQ|p9*d5j-%hYbVkIq+_`Ze}JVu~2nADWZ%Be6vJH z$;Nxwsk6K5)a2dsM*HDiITzVeB~|i85mdkK^iI65v=xg_aIjo$;!3L*Ay3T=;o;vC z9zI3!@F}iEFex<0%<7I4Orc*^ci;KzjnrCigGR-E_pZ?I4f8TU#Zj1Id){RQBJ_c- zt$s4$xLX&UZD0E8)(1!HwS;}bU;6v;arxaNWQSB0{6**cufboYxcsrp;4hbn_e!{3 z>mm+r!zSaQw8s^^R)Hb>C|P5}-FhV4C+mlsFx;qhHQj-(b+x%~PQXnU`|@#-h@6N^ zIfGanMf=@`N^=GBa zbUpd2qvNn_?<27=H-@L~Y?wXvY)X;ZHR+45way_;aUVWg26T$xdiznoT=|_NQs5)V{=AdRi^6WOA*u z+(B$8U20{rmpjB=sUcPk<#0$whH?m7?&8%mv%Fd;C(97J!3+-L&}Z)gnWy36fE)P+ z!l^3qG*sAx(~P)+?9qVt`AP*t?ZI)u8Ux8A!o3Yg+oH?}XK~U+k5p^8=N5Sj0#4REasVUK|W5;BBX;`+i)727f3MM;QIvd5ZqxfI9?j8rKRm$g+ zP@}8r`jpG)aB53S=SSDol%8?vQSa)ZGBYH^f1*EmW^@DobFB`mEhefaJ=!MA!1wf+ zht+c7aDEZ677peY$!cLA2>K>l@=kO9ZqOee1!@+r4~|X#{D`5}>gJ&}i8_?Hl?$K= z{Zsk9j@0~ZrCRY-#Z5=;xmYEsB_i2pw)rOBcT%TJhW>K(C0C&-_RK^*KpLw=$wKKm zJ{OI)G823a1DFSSq5oD=D|20svTdnbySE?Nu9aueA{(DLsU5j~OzR5ra-^2)5m(}b>}w)4 zYnOK`qnkNnnw%R3w6560n^bb^QMHU|O+F_^t71`#i*fnli5izY12WQ;q{KhH9#uX#5vNXtL4uGu`JgdvjQ7v1U9Xn zQwg&;?`EbMx!xGl+GV6M*rvq@%*z%wK!^O>&oCVQS^Ayqzoi;KKJ_t8#(>u3vze|P z^$Fwii=)QCVU0hRG5C_|dU5Z>+Pon7hiAp&sz?LQJCLxyd%J#1QC9Yq8)LHTo; z4BdCWO5LjJqw7|~3f7XB9H-Z6V5QJW{_MbN8SHc-%#c0K9^Q1m5LXX#rXBT2$`B$O zebEENoMPY!RE`$A&do)kLhH(XSp(FkGr`veL_xy+`oR_thdW0vBDis@$ce?)YR+Q| zn0H~%+=ryf<`0|OrA-vGzl!SorC8g>iLpj`nXSt&QD&Nz$fs=3+OLN<=b{sy?bUqy zV^d#w>`}monf5oaDUT7a zHJLia^hKTiRI5~C?nie~nl?2p`kJ1*U7xsp;bq;H?u*q@iY#*hA7kyaKUNUP**@s# z@+JA-x&$DXZDUBb>d`L)vp3_E#-8HuAV_Q*Y5QYWwACK8@v$Ww?|~2Qd*Fk6(bU$H zY&sRZ!M%CGwG5EU@3!V^p}q**?rjTQJjqM;<3@WtVL105Ia~u|@e>q%nY^bvj30K{ zK@#6$sJbnjfyyi?jns3$&?kP;tIbMpZ+3+$1Ma)hQj)i!SVB%KM2UlehaY{6RpJo! zD!Xq#YApykDa58M;dX}kijLS4N~BT<1*Ii5LEwZDAV zC*oM2$@i93XQfgVoW>RO61f(sU76VsI^Ss(Qx%;7Qv#8{yhN#-zX zg=$HHu#*27Fu&ocHox$jd*yh{{s@1mV4gqrtyDSHyc6u_7u&;cD2j$$Y9 zggf&VP7EI|PC_Wmt#kF-B@=Uq<09&Fh~99Km~vAu8L5`U_aJ_Oh&W2F5|{8t?YX(u z6+A&;ArkyB0ljIRoq0^j7m=d@Dx?87$U1yk8u;%yZE@zw93isn;y59@lOyV$ha}uS6Tx{a5)nBg)cc9oF!V+o>tfj}FiZNQ8DVU% zgtJ_e3Sh*Z#rj)KEIwoRkyf__^at=%1>DmL?2+_oD@QPZSOWr)X=(r`%K)A!0|+C2 zG0Exm_yyMnx0y!kq7~Y_VzsL;YbZ^D%iJ>w`k8QN9;j_FABtwvtxWEa! zRYzLM86sBI1xgW|7{C@b%Sf$D)G(~l1Fdd|jIxVAIuZT=>2YRX)knd8?M{Yy1&x`R6v#8k`{4#nJvO?FRp z1y9vhVmxVPrkES;7&qdStxXq-1cFn42QVq8yln+3lc@-gIwJ0Xodo7UWU|lr zs>ePGUE<1totUy0xdIRMEhWXW zi)+R?Bne#oOBfk@uU2tG)Wnc;+1ZtMd3t|$`rFXa-;Pk|$gS+LQr}23?YTpWGO+AVnNIIz3us|+6QFdI$tzzx@)?i*LF7d_TcyV%o_WZ`o zy=39O9}MW`7kcy>SG1{pXSB&TvYkyuqx5idk+<}KUV6BFr*CAVzG8PR*w(|1%XN3B z_QUI-v}lu`m{c4(DNPa(u3xdI7Rz!t{(bjB4XYMjoY=IKZg%>dtsQU5j;nb-l*CA> zyfG8c^OK`@{8JAf^2{e&y3&qy^Cgi=w(r(s)hS~P?s}Ml{Ul)S4vcvl13fzoF$>kN z4dwfA%Q?N#Lo7ku;0e0cx}E`NE{vTlsb+#ByiD$SUO4UI$(S8-LKW;2@SMLcc|O0% zLRq>_<~s%?yp%x_XN{#`a`a&oz=`I(G{GWZ%OXep3?=(M_63 zg2r|&M-rW3WpjM@sS$_m&62q#xHd+v_mA0Rh}~g1i3eWO?jb>f8jR9|;Jx&a+8##M z2^IYXv(h3bNr8?x^zdODXAgrz8 zlxa4iPD>+dq#$azAW9QNfSMxdl_AgiH91urSO=@Y34ubr!*w|>DKx2c00sjizGs4e0H%k2 z$7VPSo9p7M9I`Oq)bsH2nN8tJ$HF|XIo(B$ z6TS9;11O6zkR)EF4x)?WACZgfu-Clh$viwh=JhKm+pfpo%h$uZPYqpY#NRH~ov-?x z|B!EW^K%HO?#Fye4nwmERXOgfyvodzc(7E9FWTxp9C|mo#8+yOe?O$uIiHyliaWYdBxWWfJQ9gu`3^eh^n> z6R@+)j+A^;<_0Fq)XJ;E(r7E5rfA%~IrOeK+Nus)-$+=tY$>YAnl(ML+8J55QKJ3x z%=+9T+$8p2CHG6v>WY2yh)XA`=jNt#v`qahl<(*i7rud4&t%y*hqUiR#+FdrS?2US zF<6h1t)Vl+jU&q2ef8VmH%2vjPULSA6zbKCS0&fFdt2*iKH z6*uc1BE3$IaeHgdhM&2KWoC@oG7`B@Jw`q*_JOAD661D3_<4bA!GhDwdIeylu3o{4_S?$)UiVz9g@fdMl@E!h%!5o6T1Rh%?O%m~zdOSr?N!0?tlv6)OMQ!R@wlH#fSgO_Rq}uz!qYty( zYAc@(kKuwPFaCBe2UW%L2pNpM2*M-vjr_IVd>X=t4cd7IKk@fSbWyjum=BED0|lSS zk18wjoRdT#!@h4_@|=*#L-H;A03F)Be~}qeFEy68BRdVwb}>;T42ykObs87Kp6mJu zylQ0Gm1z$y!)c}m62EG%BgbPd0fcpu7 z`wP5D-Q|L_{@*R+38uei5oDW2H)Md2+Tbs{j}WA5_U_tJBb6VUvmDp zwQ#5C7Ne=rI?Y%IVZI`vV++;AY(=r2iw0o4iX(g6z*4nLE!P3@1@7a z(5f~BVrl?<+LX6sw%(@ws4-p6uT`~`x7VSya(Y1IZ~Kv5nO8o|C-c@<|BhCtmQ2!0 zO;~{*HkwK9-mKLhlzAAt(-C>P{!{tH{xb2FJTBkG#&B4HRwwp)-8|UwCNZ=Nf;X_? z)#q*w4GsVH5&0+w)21U*FHKH~<@ME32!?&%tW)m{?eSMh`6#YXXPdlbr)=aw^@_PA zpZMc!L?zf8>nD&W9}(`eD_A2fX?0;RpyRNiwIQmNryr6Q?Z#jiazlH%xepG#Wh0~ z4NpQAH@%uAi=3(~S+F3LOnQQIBu33H5v!P1_bkZrmflh_;n~yWD#CkwFvyB9q>5Nx zt+UZvYT4VB^q3Eku`axdIVv2WmJie%lM7*5ciHpH`|r-LvoiDRynivjk_e4zE)mEO z8};ZIVFzhWDs1pOv@dX+&EltqU`LvLGSHC+yK_$?zL$~sTm})|oeJ8${|a2RAlqOv zUtx@qD--w1=^1v}u_wbVCwR@y47c0?_mPulM$4%#Xa<)MWWKFN$ zlO_Eatn;^zE7ti4wS6VlnfJ_BW}STjRSm0TB~z^Pvu=ceC3&=-YWEPNrhk1UMVV;Tredggn?03S-16tLdK}g+sst=l_~y|p z69daY#eU+tIiao_&)h3B@Mxa7T^@JwOp*3=^R{B1iGr8 z57wqRrdg-T*<6#<`*2P5peNT1{kjLcY*6t2-?7UIsZW^X7Ono%f0Ie3ST8e7ayDl! zTNAfthkw`P?C^Ur(@yfD6jX?D(>`0(S9&PTAbF?Nm)i_-g3Nx!BuiV7m;jBKlURP= zQ(>QV4K|{C^My+IhnrM%!VQ@>D0w{{x#F-K-M&*kti|_^n|=e=waYUOaWOyO ztyUh;#AN)m>DfiULwV-rwQN%pj^R|$S>4VK>W^;T`zO$9(CxAFH$xSjCE&OtU z=)qduW`0Ej!ESI^93nMd^S#L5ij`>hA(>3Hx+TmkTb`c)vf=GootRN<9A{8FREm&%n)uUbK-mqK1sJ=aq$-iW$NoeX!@iih$ zxHo81o0Miy5nGI)pa0QF%uy@vTP5lN+QF*hYX@>3H`>7(KKe>a+-Xl7vBdoz7`kZ( zBYPPh1}(9WwM9+JdM*^fEajrtNhxE27qm-NP!E(6xFsa2f>u{4wb?|Yg+JlOtUoT{ zBqYgTT83r`kO~=C$D$?XBXV8MIXUq?xr&TX5u_?BSgiCN7OAta-K0rT3Hoa@Brh%- zy9Zvg$69s#9waKM(C6NfRyK6=O(guSx;Eu?k?IlSZ%c#74wy2;`&L<|km(V-G~W(e z1eV29qxpb&5c8NhA(E+f1pwdbX%pw5#O~+UCk=xbvKG+ zf6P^C*u9yh=?Kxj+ai^g=}n~*gnnzUQB&1Is3Xox6TOJ@w)n0lCfY(qoVP3KlIB;# zk;=LxdWlF7R(l)kl-Iq{UZ)5GV3eiTGM*7d6g`;3FHXra2YcDsC*&I_nNS)iselts z(CU9{uR5MCvAkVyCFa;z{*Q{fEjn7AsK$z_l?)(ka05>=G|^+JIX+D&s=ilhOS8gj z{sl!RZ}|$W@QG2`-ioX@Q0VSoGtlpKeboyCwPpSn%?H&ivSohhTY84rGXGou=M41S zle?M_AG5BEzM^Rbj21QG%my^8|5gfcX>nzlGOKvCYD(xG?_|`4iY_MF2QoQ|iF#}% z`lsRll8LJ2r(V_~Yw2$k6AkfE?@Uz6b~Ca3<4=l-%D1+O#R7IyEojQbGEt3^SYXmT z%N9~Il&o~omq!XhRCk|nydVo;;zWi4Y7BnF5)$0oD{oa(qdRZ4=ZYBEUQ)c3ebmnj z3e{q)aLHmUh0<-YV5O0qY&e>hJUyD}o>dMG3EAj{|F)6j2@$LG6X!)zlr ziqEIbHgaz&GvzV)ym&u)@cB=Wk!(J1L$W7V5sbae8TkBd!m^$!$`Y;3>VHj5X;$yC zS^Z)5S9P<%><(U{AKeP>t4&rB3^AutL>UL zUqdtt-mSDLknAe|%PXyAZrh5{f!(Wh*V5T2~gz{8D9Y#Ou!$({W~)P|Bpz zR^}K-2yCUvGu#Ik77OXj(w`(Ug4l3HM_yY?Hro=5g-e(2@ZkWgS|BL(2wmvzw%~-c zvDe&CS*Hjj-0xr!Ou?5@Qr};XCle5&k#lR=? zeWiPaGL#4Jy%b`#8gyDb_Mp4fc>CNh_ZaU#QK^i#Fc$Y(ZOTrDTnGPiLA%H2-Z=k8 zcGu~pYr6*5CMX%)#v=!J%nm6`BBe3S+xLpQ6Sw04462G8(xcldM3f!fM`U!%+cEZI zH!m>bsW`@b5yf=(#?XF%

*X>dwd`qR%A9{EOkNBL<(PN-?NdOsPQYz*?o!`^l59 zK+v@ZaFgJ(o(25P2d#K!SZ(LYhx>0tJouK@wGWy$htKfyE zAaM)0su5GJDw%|}!fU=EyiW5ej}3^#htsixOqGWr%lessf=7Zq9E_%)(`uLq0cV?k z`&*aA1`buEbu?Y=uv-3!E$S??A1T_C<2sKbK4P+C;uEr};9{gz6@RIuB`{36{dq~_ z0Pa`3D1Ozf8#e3bmb8jx-sok-v%H2cw|qS|9`kdvDlZWGy303mTYz9t$Zk}C_y`=_ zUgrtb?zO=S_4e140SLA$K>^#0F>S_*ChiCd*s$nCoO%%J-aNeBc?3zL67f~&9k7=i zBFG#`$)MA>6V zJgC@%rDJ|F*Cu|)_f#3WFjF*gCQL+?&lVj{(eiDuzK&0EIbWUU(H>eCIVbTgY5D~a z0rZCg=4WWhLC#UxG{yA(IFBUW@%M^-&K>>%M3%f?Y;+tj8>I(MEUY_1yM$BGfPyY^ zPFf7TJkRpweQHWb;@!!3iwemnD)Xs(If? z&MyD#xEZ)7Pp=I8IB$UnSW3|oqdVw*wwMZi@;)~8ux{Q#Iqms)pWQVI5I1K%pW*L~Q)U63bB`>tdPDy?0UH zDdtsw$z!g+BA%4UgC)YXRqtXxeBIaIlP9jpanRa$^FakDL5>q7y0FYAv4Ct?@Nc(Z z9E9c!Xw-?J2p@9JFXDYM6&|vI^bXjW%ojiXILX=GNP=9i*~Bques>F8M{yA$55JS6 znYLguhYK`TyIU5WMU#RyhEIDu?q@SVd{41|z(#h;FC0u#d!w`|;9N_|g>RGl&5ra7 zn6G%vf8v-TQ3m-(_?pAV^5nb+rxE*lo>T;nVa+dxDgXg@mH_GJXp2;n?k1I1)i%~{ z^5jOmKgjcPXj!vNx(c&Sp~mlSS}?_HzCr`+7P1>)XRx0(sPlYMFbMMSx!#)Ju$az`0uwf9TL6-$D!ynD$udv9CGI4esF! zhT7)`sI>rcau1&WX=FH~FgUfL@{S+ph5E={=wpIakS7{pYr+Q) zSe-Z_>12e^yYJFtHLh@HpS#C~JNpI);;y|t+}SU9VtM0Q4!B`ypC0aX2B*8hy00zi z=NKGG3Uzky5;jy^DABSb<&VnwMBp>g&yrtmv^rZjIZ}l@ACVO*xkKd<%9~X$9YX8 zv0Wm0Hxs)!Nf7@=>pB<@A=ZP)%T;Vme?`2uq4!Cz*TBa)=2P+$ni#%ofQVC?OoPHV6sbtXmfd&=iVaWZdX90wHI8tQ2 zR6^Y?VvJA6`*dZpO7~Tz?KP(p5A<62b2j&hg(!G8$)UCdFOkeurSfOA{23$%^Dd8F z<4SC%jLI^ptJ#u>b6>tKc|c*?I+Knjp5l@8-O?Z;m3*yG{)SFfMNE$lty_EzaNd1x&YkeOVWaAKPl6n-B$pZ>LUC;3jmMJDv#P@N5Jr6 zk%8c^r|bf;_#%!Im>3U6N`9vXf>a`Ny!1o9qCP3McT@$N@UFo zdFu*}PnVHS*!!*ID;0DubAXLzD(GCYm{b+X@tm2|Y#cNhS`^iPoNZ?_9$D3zUm z)1?Q47?Eh}`Q&CK3+G)m{elB4swQ%jXDTdnk7OTn(t}zMJWX3k`T;{WjSHL=r~#mv zAb2)7*n#iBKt7C(>Kx`Ic~Y)i%&5Dpu{a`+PaH=8egwY}VTpO@2(i7LAs2ev#>6;& zm5Sc}K-iw5_7OJC1K`O}wW$LAe=S5ObIrcpv5B&Z(z|80lYK~JPa|3`nltGZ?cM_* zMXP(0zmu+9tWB=5okvf|z&a^s>aoQgkL(FRAqFRPhb1ZiGCW9a_hym3lEz!Y#VW>Fb5jlgT+ZC*Lx-TdQTx=?~qB0 zwfYkQgKw}aB?LLl4MFK1~%^DLD6-3zhDpfD7Ov&V&o5 zU5=fWN_UZJEt}tyjbQ8r7++>5lW~M)yCnDDuoDk)&-Os>_3V8nkxRdYvMCI4X8udy3rD}A!Z9nQZ1{3B&mLwU;VGQ=@XlR-y%`<)lc3rF&-RGs_G(1 zQ!T!9>2k{yIh9PIYi;^oup#JMt4BQ!r16PlQh6|bR-7F))y`JpPN4+?P8;(gQ^wif z=;LI3phNk_kZ(kCp148wFi3K~My{ns9o0?zJdw+?AEJ-hAF9Unaf)!HA|X8XVUWLC zuU(vberPNOoIoq1r=TN$^$>YAA=F=9sR2L9&Rp$ZmC0J|*M8HuV1Q6Bp9(2-lRvUK zP+0@u*h?mqe<14*+mpu13}d9&wQ)PUHg0E%-Ci8Ion0HZkB{75MjF!@(3oU5-r|i- zDYmLmG9`-!@~SUdq(|41iRVTRBvLzL<*7{f8$!dRkFo=Uh{W1ymlTTuj#={Aq#Lxl zM`;bANvM-+iW z!-5rK7FNX|+@RIZWM`l);P~MZNWWRLsk*cDGK*_c24r3j` z{<(@ECTu0lL&?ugc1lNJw#Q}lL1Zz=R4FO|8QHdQ+?*&qHAc7(`SJy(6igxmm|9zG z`6at!j`ev(t<=qCN7+j;%sTwvS$X!C_&``UCMY55YVts74a>X(}aoH?F*m&uKLjvmFI@$Mb1eJIn_zd zCPUg035N{SU{a=oHpKVKKqT}NcBEE6&c!oEB=mxHYJ=3Wky^-=IwySV7Otv)VuWwq zkrTf4P5yr$u6HYTl!%X`v3$$ho# zCE||yd5QF%6HJRay_8c~-X>En2k4;t63E_juFpPLzRUbL_Fad>iJnQe1rEUB+eOG> zoMe!)k)6QqK-&1kfYu@gkUOlapjHkc7UN&yhIj+pk)|A0Sz5 zWJ@7o%scOaVkoeV0t*IOzmxAr^L_H~D$nUr5{J>tUfmh#q|eN*iKKc?_*4=Ept*+H z(rQ;Hu7boCkb-fcEBV1cv&j#h`%htcZUS?)m6v#9-(j~RngW{;VJMRSP$f1nBJBQn z1spDfD-d(Ohy5t)Oe_3Su4FaPe3qTmfF0`CdhM)pME{`*M;un_C+mxAZ(WDz)9ckiD+2t{)P=ef$NBO?6c z?al+MZ7He?HvF!<%+_Z|8R+}Bb?UP!~_F^#L-phtIiEcArm&9x?|ri9{H zCpn1eGu4beFS~Z=vw$dVo-7Ozm)V@WNq4s|YE)Hi>3Ac#5trB*y?#iBLzH5GOW+A-+?3HIK*H5f0jtYo&({RNTjiU{@_d^ zjlSWRGI=x(7)~^{u36Cg9C-NmS&~B$Ws0=(1w$PnUgxSgLf{ z77nroCmdv6Qjl;DRhxPf3+&(C)>@INKQ~IrsexsurLXy!5&y`Ed&JPD9zxU(o+Nxo zctxu4l@6%ritN>T$oBd|S3S zb~86^S|ulfV-j(S>9aM#?9MrgHz<9;{MbBb{fHPU!Gk&iPpcE%T22F(bBk9r@+tCb za8RY>SP3_h>!vaEiJ#5N651%Kw>YYV6H2&|P-cI)Ge0ya5HVbAg!wY@nS70DHslf+ z?0xRY8;zi{z!hGc$Js?|YUQzLY{9e4G;z#m(?uv^4kl*4J6;J2#J7h)0-}p6mggk4g zD<^s38tTbOp1Y=Ae#Whd%Fk(Q>g4C7H7n%jgf;ib&%iZd`N?1NeSXNar>+u|&oSzi&TUAdfzjNrNgS#RoU%zxtn!9F^6!nIEOVTkoKd-p|@p-ninRkR|G} zEw6f!vF%OrhPe-P)R6WLs=iF%`)(Zgm@t!9S6_#^Q|Tfn)NErC-o+J_H9qXyC95Q2 zX-=hH6dDm;E#o(MVL^CxCl@)vzL?W~#RUnuPUugodOF4>Zaswwl{hg>P_44Ppq8GkW`S3r;d;KoDP%BeG-# zQ$1Gr*Zrv7AmMd{6mW2=SWzm{&r^l(@KjLal6vl4D=o-tk#z{!>Q| z8!^c^${C7@d+8NzLIaqua}(-7w*28zub{$KT=*YzWKk zYedLW*6i|}|3U2~qeGgf?gP)t(H`wVqQzU17x`0tQ4fQCqkS8Y>DFq0zlg^qVVcA+ z(v6`w}-x9vmH-P}dZq!FG=G zh+W~RtsEW7r<=)s<=c2jN18c8sX($A>uO%y5L+qjm~QA@m5JU(b49|;dnB~(JRbYSCo#xaMVSU7>uU80mL}4L(5@lpn73Mgz~0SCjEy0~AGyRIZgE*L)X9MJv*e$p z-^@Gd9~oeTYnY#K{cWzLhc!yB4B85nq~J;)!X|={c$+KNyH zi@=D}YMmri#+D;7!Rg=%(sG}4Gg+6M$e-iXnK6T2{||;N-$Dx!d2^gCo2K8bwBKQB z?eQ))D8>s*CN~RpM6O%q+O(fvZ(lFrxzv~laG%)D?8xYZy~rGs#-(KrY#=#iCql>F zJn1p~N6C!o@h?^gz~oj0)3Rl|EmRUP^M6vy^d|a%ZK~yNC4zk2Wo+N#FW+fwXUG0j zI@NhdyZ4urrdYMPpE3bCU@OUSN+Qe7pMmV*cQ*$=@^c7Iq#U6oRro8GRVtrI<=8Cw zP}NLIXHvLLaaEVMQr5qHJ!PxiTj1U9b-^d9S*Ik)oyBBG{#qr|Vz9PY-^LL@T^}x2 zbs^^lA`B+FI2YoyN?#S&GMuMdsx}y*t#zdH?4b&wen<*E^5=1u^Ryx*N04gIBF$bg z+^WVOn^wT@Oly(wq_iF)wlSs%MlTYdS2{vNk=bq%Z~#O=AW=DD`z8W=pOg+q@<;#3 zL1r_h-wO@J2L0**#-B_HLJR*b*QoO;Wvzvn)R}&iv(_^H>4zElaa70M((R4bROXHR zrY^OAcTKf_mtSW8zR;2WR=Hp{5ww28BB&`-&!CTf!l}Bgsr_2qA>#e15aK_01cU$f z3Vz%hg99yRK6OP#y~uOhl5C%`O3_Mz`5gtqpbNM*!ehtslVi2n&z_X3!ed9MXV2Ns zerG>>h#&eZ&%$Hxmzzw#5s-dm@i;c{`9lswUSDrDmm1jN3wi)$v`DHp*4j@HjJ>EF|lOKqjPW|Vh>e|E~!rfrSYn0o{OVv z@JqoX$xyl10f!+l<~6>&PrelGJ^7;D*SCR9+U~zv%&k@l!`q&(FLUJN2G3e86FpTJ zSgC}OzN=wvIWL`QubpcOu>j~XBiGu>)71cxV{Io%3;neJ%^_H)j;B;#x>XFy-|C1< z1;J6Ab2u@tf9M97JO1|I*MRbJ!+sfh+y88B`k#MI>ahJUme+qQIjC|bck*XZqwo?p z6+9z8YJb>rE!3)G19L?>858%{uMKWtHb`VmxK+)8#N%`rpagqGywaX=%mJPg=D2f7 zHyDoR8sVl~>T!hL<(2kK|I)b9H}I-(rkU+W_^*-zOjWoN_6v=g`Md9e;KY?szZ=S8 z;{kf{YZ3>MZ#{B{I^h#H!90D{PwBDco@YO)F3?vA!CGhWKn(u#L_#spBQVAKXbuLD zDlEk65-|Vfg0jTW;32q=r6pDP+0*t&T`2C;3f_^{Mj}5w#E*%Yf_#0=@^F@lQ&W|S z-Y9y(U*%J?-X*X{>g}5CI}%6IN#Lk|OhzhG##f8)BA6;XlMl<+o1Lk`o1_YAH&TVy za|tR7uyhWTjC`UzJvNsfnK40zxE>O-Y}bQe3)jOqTn{*sY}bR*Gh!$aN6HG!2f~(L zQ)hIcH6*OkLDa@bkuAPfwgAiZc`DeIP-GSBn#m{lQmXJTQ0b8pFKhHoYXfHC`QLL8 zOA20jRs7dG7oGum^BfBOckTav60Mi1irNL_-mxPZ+7O ziN6XDV!mH=C9M^cB}nAfL4U$=l`7o#q{91&R?CkB**jB(H&YyOBK3eecTKSX;LPw` zEK3J>FT!uK-j;SX?@pXGf1Rp88!OCwqOPtHM+Z%%3H&PM+fs!~X-81~a&nM$Jx$qJ zCt2Ro4fHZ@HRT`p#ap6GPm)@t0_tVHX?5pv;UUN15Wvvt#&I_%a%Q5KUrwqMf-18b zh_DfaSFBh(FmVV~eKcp;onZ<*NNqa=(CGjTs#rj%#b`~AFe)FDCPHT^B}iM@cTU8) zng_IXM~NYJcU+fg8>AT*w6a@RllfF(t%AqVy{{=Xm{<>Dm=8McK38`iSbCP}A4sfH z{VwQa4YF+xYXGxyoOHR^>bOv#KzD}vHDfj_FQ(axJr&<|1S_Z}8OHCSfUqk_f2&K% zDCwT6#54w4k5do0sh0mubK8xlOlZBijXP+5G%x)BNp4H8Kk5xgOzEGdi z#nm{F*%FFH%d#~rXt-+_LMI5{C8O&4VD4!{G8v6gCY>BhXh@#S)N<6X#Xyi*O^TpQ z6^()-;(>?kw)WmFqvNvxe8oXEJm+Z7%&5R3G7iTirV>P?IWj!dn;UVG)m8g}1ld@p zQLR~&bQ|GSGE_OCZ)?x^DpsesoP!p5shBy3itdI#uUW`^i!~H3VkLeEID(8wQSGu$ zWm2i$opx=CGTDH7pjA`1j0}$Q z8lFc4u5#B}%h1X|(~$5of+$y{yTR$6-%l0hJ}w3u!a}|&!*VpdH<8?6XbpXo_$@aGXWD)B za0v5U{3zT-vdf?ihhHJze?Yr?gTF@~^+tJ805}>dU zw}#$VK)C=aSeypvljLxG>qD?Y(0`sQO_-ve(0Qy8Yt8&`1qGj^D~~IbZW2_sHxaJ-QEc~aGR660!#n}3 z@S|iTUW|uA-G{=@Q1CMJHkH>T`_IuU`;n*@QH0Kh4!{``$;?u4M%2)Bbdyl7(srbuE+~LjdRSn+>7&RBr}LZ?rmD z7&ATkAVG3IB%ZwKHvGW?1^ZFhl(^`$-KZD+^SKd9|0JUp{B%VF-QJ(dK@08Vc{dY; zt%LUR{ZjjTO?XW|>-SiL#c;mnh_8>`{v`YRskB~Tn1~ERrJftp_00du^?X{?tsfET zhj8dqx@kv4QRS|NwxQo(A8Q|~l|Wi>L<=4?-+eHe8Y5D@69ICkZKj7KK z#aJ%#6<9b)$vxOgEc7Dk+h~2*F5FZi`!fAyUGiDVx zF*J!(&}#ic>NEM+^v|;%=(?F{)pgy>vKDpS+-7~N>&CEd=(?$~YPxO~TUFVcNG;G? z&8_d5F8g*9l(UQ4cG5NT)qrK6>T;QF*FgH_XWWf*jB_ z-YTsUH2qLLa1T23`xuH&WI!LAXH(CB@Dk3k&A0M&ibZ~Df457r8*j93@yMrLUO-!J zGzSH~i*$@CbCwk?ml9$c4307fP2f49HqWbv8?A54*ilWjq~^@Xo@v=IlUqAg_`)9$ zQ3oCT4j!hRQt7GI1?qccmm7$2Cly*yo+@uo&NByXr<5ErZ(TF%}>ch}bbI`q%q(p&gY)f*Nh!-2N)l&TD5?#jD z2D~97fX?W6QjwUK1^&n17Vrh13j76>1D=5bzFVEIr#=+$TPX?n(^W$o1^kWHKpXsj67(ff z3PD9_wck2lz|7d*x^Q3hd%@WvraH0fX zS-~=7tNfLBfPJVXk0`-Ht3U~4$Pa=)CPOljvFd;azBLEkBjp*bf!0#K4-X1)pRsOr z=}7sued~3jGB-HfTp=8;K7S;vH{`Mw%lzOx>kMsY^s4+Z^)ie-zj+@n$v4IsJ-@Nf zuE5?mx4=)T^R9fs;eGSiNYx7HzVZ+18@&x@$$>i(mH1%EZk}QYT&0_b{ANP)cOukV zH!r|RGIfA4yVYiY&91|~e;PN(B^)-wSCX|;m!6EyX$KT5LKg$U7Ps<56A=tNuPV_dMz!ANEl7_QmYUBqi z-aSkONoTY3Jb!GF17ifI7OYDS5mgJjNag_FD5p60t*$>syGPvlKJA$%|7tGyrxB`k z_c>IlbLJU_BP!l*&C#7rbHbgK3!QVEO$~igTZp>w#}j#T^l;;sOE%BJ-@JJ0k>9Y% zX5sk;naI3R2T=@b9(ywc8io)=m;AubKB*HpGp;g}U)wY>nTIFU(J&_+B>^Aj5%zKU}Ln(wgQ_)flF`(ll+;f(6o6}hxb&i4spWstG@l#)$- z>kKVOUIRO|L)-zi^+iOFgg-O z{Bwk(MN8wZ|Lg{d74G%3c0SW{H|Py6U&C!=vp(cSk-trr>_ACP zTKfyc$l7MMU{J!CG?!S`1s*Xf$yqix=+~=w=p*Nwh(~NA6mdmq#&spNf+8&L;#o02 z_r_-AvfVK@!x3<|`L!ucSms-;3-ODUua{#hJS2|yob8o!%gt8__h;|m2ih+i4TS&F ztp?|g7@C(%u&su3i*@p3#X0jiRwRkZs;=Zwnqupdmq~Izm((DWi;1^IMf?;`s zwc-Pma+K(v_2aK!44bRbVZ87kfyFk>eDECtjO{}N0RL%)$KaV{G{2Z*(whwDj{049 zphDs{-8>CbHAi)xbVO z8&90I@XA&oaK3JY4>%0xYewl8Fdid(2o+-3xnTSiYquqVgQT-kpiaw`(*a-wKEu3} zBQ45;*LclK^K@*wOZw^^JDaNm647^L)vfbgHv0=gM4&){D!L zgN7*}YAeey0>!2~bd{aZdhZ*cYH5j~g0Z`d zPgq@kpoH98N_yB0w@{!pJB|{S#8RJY}TxAs}|+u&S~*FQrI|n5F+m2Y$5~XXc;|xcV5o&5^P%bFgY{BG!3fWZjA@> zW@j0NlYRZOFU{k)gZlQn7aOs;j-x)2HRC1gbIiNKXUBEpv#y2#IykRyTyk;8P7pCK zhtS>TWw|)KNe@rVLhYn;aqk+lah^|QRAF7OSWk+bcy_Z|0i@y~*QzdJ>`)fGt|&s> zuVN3oLs+|V!7e4HRigTC%$yIEUSsbn(glAtOmC(IZZ^MZ@w4Ba^7iy+-b3YzC&+9! zCdxDomUz{K_c9sf6~}4F)CWqYy`I{v=DSef$(2HZvJ#e7stNC6!Y5c1CHT*qY7KXO zx%5)EUCFY^UibcG1EUhh!k*iXhAL7{+Kiz`W3-|e0jvS8Q0%bYK>xOlM5gBnM>~Suq>;3(APt*{W>sC>RICUsPn>xU|bM zc$gVS`h$mA*~JpZKJ#tCHv*$G~Yb{MNOOFu8yh;(O+iL*OkJ)0fVYqv{` z%S-x;HH^TU4as3r%@_<-a_EK;-t(n$4Y97VwTP?*oeT0g@hW+%A497TcheAiZt%?S zK1c*;Y761|+XJz}2e7MzKXy2`u}H}t{JbW-XjQe7T+A5AiGxpm+~|$vv54&mIOEX3 z6%?_qgM#M%Sw3|(dgxgZEa_d3_nSE0}vXnhB{^Q}vi z%{u4;XX<3?EQG3%Kq8xb%^gC%EEOBA(NJG>Rf(3MF3Eflg!`v^-hBKbGU-3VGSDo1^d>(IG& zvB{IU5iQk`DLFw(e#E(AlpH4|^98Vwgg(SB*m4u5&)5Y|@Z*nNE=dj6Jc@VJT2KP% znfaY}%Io~oRGqJgxE6>^a2n>iFlW}p1~&T5yf#FuR&4m<|2&wxs835D)+q_6A^L5m z3KUkqA2~w5uw4;l!haz?+3SVGV1S-t;lH+CWKBojx5}o{O|;cPivYkd2M(~d@dSXr zY2Tww#miEKk)QBTVv)mN6os}@+~Bq4l)i??y1C}9+RQ1%@r&I~dB#g!&rsLqpc`%k z_lL_d&Y2~Ehih@urd5ENb8r41sR2jhKZK-|WsTsD z0+t*X6N7+{D~tH`pwhXluM3WkM^`KMDOoXQG^7fD{bNxN+vj*IgC#fHr|!zKdz(_f z?xSU6c99XAm-j+WG1tyFDl4==1tfH1fRs&>OHgCAD<+TP2ZlXMH$ODO?o@Cpe-8)s z_CD~L_C78GdCdW-!bK0MwG1tE9dwhx)FWC&{9qpl#%~U6Me=Htc=CvM>$qs22Jcq( zB@wRKY>y&D@*CM|CZPA6eRtve#qX=^d&inbB-rD?wAq`D*yMSt;kbnB#5?=2*kb9Q zM1Yz1O(?YyN+n>he{JB+irT~vX{*`$X2l9~GZMydVW<6Jo~z*kspP=bSfx_Q{MDG; zaJByGK^Z(tZFr*0Lc(;35(@KFA{J3|i?x-lY=?W31RuGfzS4jvd0zOSll?+myV!3d zkBk=JP_x9$|FKwm_>%Kn^ZK-SisUi6B9Wuv!%nUKZ0>C9Mz52`o--<d$QN9a9 z#vh2KfsG8b-eUE)UZTdQ9CndHI7}2aX3q)PyQl1d%(U3#$pVNu&@J}B{?-Z9VT4QE z_ViluMGhYM`9z+vTqp}k+P0$3^A&d}le$3>9E$|kt=3x~3g8G7Z2Geawaca_4znU< zbzZTr7W6Z(cU(v7YER{ULVafwLe+)^v(^{Svou zo=27t+rwzn@d8$nyJQtf@0k?5d%*kbgO#J6!lV>D7k2_{V6hcPj8@zG%q_7iUDnT< z)rqJTL4;&pryEM11X662z-aOvUuTN4V>0}<%ht-+PWz%Hwy^0#Inmg9y*@ZtTA8w& zgh6=s=2wxtXQyI47y?NS*04SImr1Q5PylQJ27y6U{NtIqQ7b(Zg9x|H2&q$BPP(E5A23O?# zGDk4~Sz+ZFlPcRu7O0pzI*Xt6Cz7D&Sh`wS`7T|e1_^#ZZeAZ6U2)ww(O)hru+DoU zm1?*No7zo8_uN#(|HX;Hv{Nx6G{dO8t|)YASJ`~K>}t3{j#a^p!NHS^CPMAVZ5Uj@6-5$BZ@!)oXS*b1nG-2W{z6B9qrc-MbV6bl*iRXo-rt&xRUt5& z&|{ML7LI4WGtr04gUKxA4^H}JhIs{9PYx`ss<=tPeno*5`lo^&Pw$1+AGtmX9KYt> z+!G$Hp4cb2$J7D^M?k^BGQ#vMR&e}{s4)a1u^tAcq--<2qFCe#F=l60*BTKo5J8Br zRl3SC)(xhi+M?SF-dF$QXuOXG?{g|ggswUg@%Pz9M%I=UB|l@S>ws?Tp?? z!aY0Rp1o&Uxglo}U5>p1#c{YbmR{Kr{VlI6weeAq&$-b?WFo$}PqgLYgj~kKCdQ{)8eZ+>k^h9*!|b6k(Uca58!QxGkU6bt5_>5Mit3{xXE^t7RZtlW+awAghw7Z z(sU;O8Zd9Q2=3pV=meQ^5iqyNGrzfmlwh=1<1cMNqY9Y&{qe(2Kf+7Q=u6T(XSMrF zLz}wA;8Tk5|SRr!R`t;Td|*Tj(X_LK%~0Hj)TNkkM-PoVzbb_ zcy@vdFN+P^Uu||6kr_M1*cq8IlltkWxzS&`#fVh5RhxYR=7;{&x~hsv<1mSowsCDZ zXVg?PSD2X(PZNf!)rS>P_%L}?vv$tdd08ayJU^-#GKLZHQR}Cnr&-?crx=kUnC;rv zaJifRplyHYo1lfhCDVO-Q#?j!$%Cz5PzY;Gb!;KwOJ9Wc$p(lh&m`9B=OvFylZ4h} zjjDv$r~vn+=>T_{8AfSXc7_(RHiYb1h{W+pWIl3tc{>r?0dp_MP#`XH{Og&a4z=X_ zX^vyyJK7AlwXirgCB1J7MsS5qVg7QK(cm zcf^c5tCl82RqJcb-zkl0LK7Sbq!+`i!2HyLP-pE`E%Op;L_5r3B!T zownMPy`X`&s%nVWRb4uvL}&ucP|rN+B32i%u}7BG@_z&W-%l@pYLNz5^>Job@08C2 zGfi4%wR?8~9@tFpjwsN*cSpQZy}L>PsQ%JgN&jGqwe{%!q?Yt;sjWOGG$g8XJW;TC z(m5nLt5|{$lzuU%(1EQ{s#mnClHM)*DJO3;|863CASi<$;XXg>UdCIEjQ1T0mg)MMMjTJkb*xiX%O08@uHA;cW$+5Q{tL~wr!o2m${SWPqyUV zLsT0f;+M1Gx^YJ2IwZ8~kkGCxRwosUq)?_L1trc&8S4td09TJ@(nMUgP*-HWmQc!* zIi&z80J82?3J?iMgn$zU0r708OQf*h(@ofxIR_!2oj*unhDc$|#)bVw0=SSZODYGomR4t}B*^57r42~<57aa*gfc?BI+Y8-509y%s$Le5QtO$?~cu!&konV@!D z()y}=u0>Ovmnxi(m;&^}*r&i1(7$vmtgyU$1!1W0IOME+jX3B#a zjDxr3SUtK8+5gy$46$XSk2v9L5?=Wp%oYf+7N$%R^TB8kY3Dv8TVz} zd${*-U&DP3_p`X4#R*wFn#Us*afewWv@)90;7Fcxe8_?J>vC=*bFGs+-O28`Z0&7#7jB^zTgybq65|Gr-a?%e4J!?<;v+u)mb7Y@WchpPRpJq8emeNSuqFstDPU)#0 z^EFYGMIkTy8m8Q_%Vq;x_>04#p)L8};hH;Q>1Tc)?o$~4;!tQ2ytl*BBO8j>`bVNulJB!%HRm0Gmg+SX62RlBI&WO2*F5*C5r0*Fh% z?S@f7tgMy!y`OXMOqOE%`F#KW%_}$Op5>n9InQ~{bDr~@=Kz=N(<>5(D&+}p;LAAw z5pU=x(wpm~4J#5SuvagPDaU(`^prFyh)8`H>vD$0%UG|2Dl*pVdP`00L-Qy=9bl=Z zd>D5_G z-cW<6n+OBOkT#Oh&L)i$X*Ol^6=u}E`p*Pd9MF|LbxGGxRhMJ z$5#!AY()>e%ZFF;p(#}fJt}oL8XxCNuFKEJNL!I_uL`FhjgR;B6Tf4{Lw3Xs2oEpY zy!@ez3|^H6E-=A*v42x|%D7z$Oh~dgN&g#^LL7nP=PpEYd2U7;w!p$?vD<9<;eOlY zy7lGt=SoH}N5J*n8kzD`*Fdm-fizKxrRY18V5yCb=zZjhUggt%xi#C=33WPGa3D!7 zPFPQnEmEQPnb4V1$Lh!PO%mqHbtd|IL%-L6forp9vC;UZq>*)uX>gC_#aPQ^@m5aZ z$Y`WNOa9;JM_xj!kTwh-D(WVf`*Qc3I0*-n?Q-ZlhMg^2)9KVnKQ0W{NTY3Tl;DYd zV%qaIrbLA^5M2hD-&m^1!Fs!K7h9E5aYUO^VUzO23FSc|77Iqmbi~UbK*!c@EGIAX z6S6-BJ51mkLT7^Z!x9)w-HT)V>0QC*)DL*a;^SZA)-pp!jLZ1H(f$^Jah58d z(N@c>H=O&F%H6aZ85u*d(W77^rHcSqHOZlPE z*H4qPU>Ky?fPMoTCiT}-e|N&S%;#e{$ZxL+FO3+iH?;cEL};sbMACDYm_%?e0g7eG zJ?|6Bd~#HAfigUY@!>^J)y zfsRz)S=d{%)5qcA%nE&2Aal5i{X-1xgqeozpUX>cUf$)!nx(Mm9S&9|btKbo6NU_# zgABXjTS@}sI*#>h*VR|Toah@1IOLcgn$!q7xL#*Zg02|s%xn5Mu^?e9WXhFuuJ7uc zTjP8am_FiV2H`fng_$0-6a!% zG~eZCf9rn%7*fHdhg4fH3LHwrQv0D)Uuy1mewsOCxHO7p4(V?-a|n~FYUYqbXc)-- z2WHQDLpP_W{+t$?n60gLpL4t{81D_;7%$*F)N!t0BHbL%+KSnc(Ul|oW%xn~Hh05> zGId{`MCB%yo~{!fTaKN#q2gomEsLNO!h4KGC&5|2B>r6gc}P3x3??{p3*`qRHd^~u zb~Rs}fiIDfJY9yY$WdiSVcRx}SSyfMhZj2*n6!s67D>NqvGlF|UiNY#=_5_g0zB40 z5Izmg|&IM18_gR?YTK6ot@6vwPT!RJhQGPZ;!r4ntJpL}|GhuNnc>l@6mV+3rC zc*bArKk5yQzt$Vr7?1Y~|0nq{968?XZ^VUf-5Bp}JI(G4{EwRu1jZ8KK(b>9W)&Oo z>eECo>LNCiEWPb9Di68wP4eJl7Px#E<(FRML%xEaK9+|;k%T<`s3;XYw}IVIA zrKIDaEb5XkVk~pJ%4>sHgiQG0H2tB|PyIEX`6m5>t7RHNDUQQhTqrZHjScPXk%JDBpqom z{AQ9YGKv>SyM>wZ-w;Xv2fo1ZTl|wlvtAN~Yo^su`g1>L5-)C2eRh!jyqnnTghy{)8wiO(h-U+Po z#_gQ8Y?T6Ocr^L6kubc5msVbY>{|HN-RWD(WRIoAN)j&JVw`W z4wfS@2yOteb}$*`hwS5d2V9rp7&aLjv~3_*8Mx0gaMz5)fSU>+Kj9(B zssXAl3cdD4ypYdg9Aga}hvb7*y!BapTp=IR<%-MbQI|qKP(}{OBj3!!{up`qtUNdk zhJvbDiA?f|zOVGzto>8fq+ybcIs4gD&1~L%lx#+m4H;0Gl<1cq%J8&uyo_9;XBJdG z``3_@VLXTD6n3~-fNe_eHl6`S3d&p9b0HJFcUFCVxO?v_Q5lZ`XwG`4pac{|6DE)1m zwTOjn#&&ekqzO0XTUvBtKR`Ft74>&z<>XYFb~1kxtVDXE55c=|$FglE;dQ*rTmgP! zaA1S%&Oq^KQgTTg*Q0tM4|)YuIjloNMN6Nkc@$mxLUla0b*aWG|nI zlgnDke&Gb((2+dW7T7=h8!ucc2~Aru=01P@X8$FqNiDmPbg~Dx8O}q_(av_)_W07gxgYvJaz8?=BG7W&8@QJP;&4VPeUg~Ec$P1)43y?Y z`HB|%-{-hc?8c5xbn1k+n#xW{(dQv`lH*N2mC+s1jB-D+Y;Ky(l)g$GMmsw1LokL* z5A)#wML{$95>IbMON38T!#JwbA^SQXszuw7UtY<{tgWK!G;YWaf%jWmiM{9}>0nFQ zN7b{NJQ88#->mO%>+=1x=J&|OI*7H_Y!)&wUq*G);QcxM@IAU|9AuB_rUg?yl4CIS zTGP4Cbi6piOG>>Q(JdzttYeCC+?VcQ6`6}%FUPr;vxSEym2X(!M4~T0aH1EQt|yXw zlLIG`eUkzwG+$E46^7YccpsOgX!RVkH`^}^}lRqCaeR{x;9pc3mF7f~8t#=y-o za~Xi!pCO!<>;#Yo7^4iX&Vk&)dKKJFt;cATTr$V;jBuX`vl4~SH-bh@{v-4W-=AgN zcSBUBV4VLd9MTJ3HU>SH9aRlrMHHHJ16yY-6+)A)mKV@!l1E;8^O7mMYJsOS>nPsl z!5Cn&X$EJ&2XwFaL5Is%UrrxySABe?_pR+=B*<5CZY0iZBK(n7pTL-Ky_$x(iNLAk zg_Gc@->b^DWR5+m_pRF`kQ)oo&Zl<&K%;`% z4N1S24lB0z!pRSfS2IXXUhaoly&4WwL2$#XpCg26SeNRjcddRuRsEz0nw99ezT(wy zfts@8toIgGHiw!TB_lR*)F!67A6N1vEKB~46}c=dmMo``ag6nb%sJEeHHZ>^9)*M1 z)K^An9q7p@EyN7-9WBfAGn14eA*3T}sUuM8pd|(EUc_d|(hTHkX}q&4mQ&Hn5GyC7 z3o_LHp&VBCd}6#!DRuXy*>sOX^LUsPx}KR!%o_V$P#V7iP1+uOj~Ha(Y9oh(LCRsXUU&1KHYE!>E zo>bk+>j{_i&+){C=dpk5Kq$wh4v4Qcs?=v^ObbHzLzr_nB(BxopT8n-J0{`5I6R7#K-F17S>i=7k?BhC}ImG zFVL94cC@r~IwuQ^<4(zCr|iG`{DReYK|7`|MZ|}MVFJg);uc)Dv!`Kd!P5@*0Nx-H{qm&$S#Z4Fqd9JeEB^yCp+9hm@yUUjA^V#E%agLX36@rP&%x0C6lLr@PBKhzMH9kptMk$_k=Vr zAiaT6Qx0(@9GBBse>V9mKO3Cdv7m1zwlZICTBBJ$$ajw@3V!OTVYt7LIl)GVJ;*>P&lA zG&ddWm3d7{oojhwa$X=D=l{~xcUK-KNtgF87?u$}TWVxn?Ck~}7FI3W4o5Iss4s1& zf2COoO3u9WaCfiK-79-cvBV))j%$tDB@3lrc_~zH!t}$U*qbIRb+V)578&Zu*mU@} zXi%dgkMn?CYek z$z+uza9G-LTGD^=H&s#^L2uL%z(qmEPBSb+lHqMKy^@Xb5A$8xZagE&Lx0QS{WTMi z_p)(jKX{1%^SZ;r`YJ7_v!qR_#y#rOenySBfd_yswB-h1i-x(Rup6^f40>pRO%Am= z`4?G}oIzOAF@}`jEuSJFx2HW$Mc9ByQ-dFCkV}ys+qnO74S4$i>jKh^HEn=|>>vM} z#Pka3&(T#v2xO{pSV5WnpTA1&6qz(_hmikE@(JntK|*7>w?=c1jUP+_0TI#HJnW9k z>x!L;1Tg)@&@<|f;%C$!?J3jW6+<5#V__&NzzRbp3Dba}D6$$8MXfLkM>*u6aMZXQ zDA1&=F6%+#c^FfDxokjrLu*!2?S|BQ#-VslEr*Iw=VB#LFn+yDu?41lOW$E!AYHES zF(cn0QfN3j=dejJKEpU63|7~*&`B9TnC9B-b9wc1y}9qn`MOQvE4;x8B1XLQUuHgG znYdmOR`6cTi+lj!ue?}>%pM)$Hny1aHxm#Sz>Eb;oZ@MB|$?k#oTpMk+Md#J!UcMxqBl#O`V=+W2 zmyu2I9XZ|TT8z>8*#IE}ubJzX(i-3~ab#G)C3Qxr;_OE4zn`_PF){k`4G(Xa_~=9tvRxQTN5C9@FC2==;OaVU^+;B`R~b`IC7la=d0 zCVrYNG$U2>D#=~b!SSwOzq}D`{`9ZXoyB2b76Y>szrx>k&J%Na+l-bSA4 zWnY^0vRPhE5Ve|@=tTs3}N=JI2E2Q_*|G`T|)dwCL$8-swAaU<|NqFvk?ystjA zlnT}b+D{+b<3a1N>5#)cq64)&eUlrG#;Nq}--|Q{Z>n=Gjf_WJr#;v}{l+zJYRHlW zG{ptgRrEo1eZ1T%KA9Xn`g-J~x;s=qp+1yJLaia2V6Me>qd)nHC-fVUL%D+`nK|4T z9h%I!?2m`Knhd-C%^LzSSN1nLcgBAn*`c>;uelrQ-8xr3?dHn)M$XXc>$CSv=ICkQ zc&)$pU)Ubow=luAVw)?ljqR9PZJDh8>Njf*^^();HS#}~3+E>v_UP|=a{uYO!IQhQ zW-78-?sN`$Bi!EH+pXs})Q1yLZ{)N?FMVdH9)@w3Ys79+A)p?yN7*hONtObL{P>TOE9!~aj}Jx4Eg~d@i$W zYL8EDb?J?hgQW80t~YU`G515kgV(R1W4$3Y6a*1n*~j$L(zvEWc8*DZdO!fB89U?wK`sM-vq6z7_5NY}U!SeXd-$q*zS=b_TV_N8|qT{tv1)`gAF z#=5X!pH_cTSf^7mK}MT>Oh&9U`>p>=vpW(&8c*(SYYcew&+|in7VOX0rgTs*X@y6d ze8B1=uQsJkd!(_xQH&>hJ(_1X@Sr?;L(s|7@QGI6pHHUfz7X4ZO$|r=!vL?=a2S|O z9`^UpKP6RYah&x*#~#8xaty1S*q*;U(B)|?NOFquRm}R^isZok5-Rvw{Q&8z>`xiZDcrjw z1Z%7syjsH~m~LV6B(XD;SjQ!a360sUTK!UBiWL~W{=qe;(G??;9fUBPd_QC;`AWej zDEa>dpJW(aXbmGRAo+jqQ2L=Ya%2N(9!#oN|A;#vwfZfBBKmrdzRiWgNu)`4KU&YW zKDBPl4<<LHsguJ-OTcA5flF|2SU+?yIZS0(bMKd+JEjP#^Ksbzrsz^okOT@G94_ z527UR^M)wNb9W(mkAV7XO_H}Cj!C<&N%51d`tg4!ctY$@xPTMuN$&Ks6uGOXi2oJ2 z3p0UrBX`Rb;A9*^?o!}{WY+80%2-#lu2~IwU?q~5F0?K`RNsTp5fYRF3na1>>n`#mQD>O>*Iql`o$5iZ)txg z{rVB~OQrAlfbjo9zc!wUe!++*d_Vn?*skgC zvi&#uHIU3i%c;5>5eq9KhABJYrTpL1^@@C*&s-nv`Uf?-e)jlS*PndK?0QAJOtSSX z5Oin5M?$z}JZKRvH3YSWg(l&`U``#@v$7576WqP9A2E{U`=J^Us|i==^Mbr|lCA%W zK0>h0S0iFrj}f7U++4C#L*Zs#0;P38CPuBoBNXC1BV3AwYwloa-8yr6FQ_-BFB3AL zCVsXZ>Wo!>=rB64*a_1bR?_as#*Mu%^@irgDaM6Ez1<04y_$Zz(LOWCB!3pya%+@p z8Q7iFMidVd&OJjVpOHz(s;6R)jAwpKr=`jEL1cS4DUn^6xpB*xDMgfk9)GYpvEk^V zz9QC*Jg_i60kwpXQzR0R&G3)8tzK<%lgOBJKho+&(B*nvY~OPl*3D8+&Fs5+HcqH| zd8=*9FLEJSCSN9N0JCXZ{Bkj$?+Vp1snL#sqW@Qzzu(Xu<_@29AjdetpSN7V`2 z_eeV%gmk%=k8tN8RPpsSg({DXlCcv~vKjO^N1SE?Sh`+hH!;cn9k<=yg2ZGw^20C+ zt@efT!e*Jhj2Ex|r~<3L-MHo#YOg0b{A=PSOQHkxWIofn1N;_!J!9Li)G;+vCM|NY zvC2%5YNWB_btH4IfSNXT+B0*93>}5YZ@#yf9SQp&-o1K?$YcHS;hAhSnj4d7w5CQb zQgJ7Ylb6bD6JfpMmTf@$XJ!OC6JmfTX}Z)lwJn-@IdvgXllTLf$FbrNUS?M07gPj1 zA`7v7!~Tzw<(Ye2l#G1VEc`-Mjb7|(4Z*Z>l5qt&9$s2J;0-7blE5J97{P?#6qHFs zVk>%CffJ{Z=AOJn$cx}xcyhz$MPqVPD^R?yy}(<%8In?{D%fccCI!JyVzc(LyEP}! z?hK!0yb}A|nDeA6N$oiR8klQ_Nz|S+b>e7U>b&y-quqFdq+WfRgOEGSkY#2_79lgu z5Wg9MzI^IbGo;uI$sxpPhWx+`aT0Qc8FGyo;w9u_Gi0Rn6f^KmGf)T;DI-Hv1J_8Pg0v}nFYAOuA5!I+ zsUA_EPD4>9^%^sz&J00IBK2}Jq|ywTMabP|$Q(1IkdPuXWU7Ri{n}lx2Zhg2A|hc_ zdXB_|eM7w?zD88ciTh)s9|{U38%dG_Hywt!{VVsh+=!<1K4ZJ8sy}5HLovwEh`d*S$5_gM_J*eA@G82^A-^54oV=%)?=Q)_&3u>5P~)FZNO}ba(SHTLcVGj(%XVv_H&nrnd+_!` zZ_q!FL&ePb_G`~(2#?#~4SHDD9P}vFayjuGDzF*n4`M98Cg_8?T&4$4lD;VA*slb2 zJo=6TeZ9V8&@PBj7DS*V(_uqPJJM{}*gP69(YPVbLZV(dwIl zomr9OEo4S?Y3!bHmVC`&puaA*1!nSP;`?HOys-=5m`>o$5 z+XrxEY{#aFo~xw04#|S3*=;qB^pd664y7wiZIu#|IyA=C#I0=+2XOe`(mp0P48Ke}ymW#T!aml@$3=f2L)|jsu|+S8+2qv=9Nuwn8pDoLWgYnH zz*h&pq0*RHf==7)-Om_1)_EyEH1)V~C7W6pwo)3vzT3Q^=WJ)m7~SCGw9F#r=o?s@ zS!UZ_eV4ut+;ALxxuKsoc%{B^({)KUTi?DLuS@A$cUS6h=kiO{i90U3sQHFnH!pYN z?IbV%8uOOIO+Swzi_+$Hno83kjk)*2M#)rb?8KDrLVr;elF^9*+;(nbkrOIk<_Y*I zfN}VhK)s?f>(22Q8|UgUF?$_MU;5XefCGC|7a5tO*rO4m&ZQ{p9|P;uwiA`|i7+3z zKIjZ2TgF8~H}p0RtY)&)Kho_#x)9_Q)w0~fORnI!NYMz)+fA7GkuN?x+*rx@$qOca z1e*Bmp+za-0lMAE2Z2Us(0-+S$*k41Rw#n;2|2QFwZdE6#{7F8*`WCoIAB>7OF>IJ zk=2B*u_Gkuj$k9^d((YDw?_Am0ioG$MTAhtVCJgH9rD);n7Mw)9vkY}zb7v- zw#SvEO{cMBHnEX1j~;a&&xsQgY)g)3MSRy7iCAog1NL7CW)95W<4Z=La?lw}m?`@M z*U89BJn9YP^RXDSGG1f#WKN#OBfJI}vZ4|fqn$un=4@rAPw7I-oSSZxnQ6JdG5UG< zdRlNt-#WXpFY@+F45}7m^yN^JvEAIQnwQh@c4TBLVFRmanh!#aq5)e6FUB zlmxbzwh)%wmfYq_-pQ89mZtWBD5s6Iaie(C{*2_NWVy~{n`)~xl{mdkvX2k6$F&`# z01SjuE*}$^K%mIel_wETLZiG(Xop#~XK_}-erAy@}^=S!6-VGS66q1}2yPAwdc*@>usEo>HV;L}K(@b)g z4?h%|l2Mk7Dmfr2ONQG3Abi;tgF8mwm+rp?&6kmls0t;wB27rrUcg92i#iiU%eaC& z1L|OYA$Zniuz$!QX0ZVs?;5cgHBwhoyWO2Ef%qjnft!)L1td1@k0+q%pq)h8%gv;7 zu?v~p#OgP>5hgk*(B8;hPuHM~gj_0G2XjVelXyB^#SC7b%(xrJ>{g$dMhMze%7{*f zu>0r9zIeK^AyWqKKq_vO{giDEFW4?7aXu?(_{6v08#o!SJvg2)Z}7=XujCs1Q>Ih7 zf6vTO?yH$&ar1+(W@d>Sx+lUV&!_VfPRKzbRLih}9lgI%&a`aG%ifT8j46Ls95y?X zv}M9G6y$!lpmbVj{1~*Iy!z?9++zzJ+EAK6U+lo-68CFBvd{z9ZulrKf%wRRkGZ5! z>@lIieLhN7+E6#cR1W;5oV`5p?B%S(*jg90orBo$A^(L>i!8$-yS-v7OkNQg{zMq8 ze7#~D=bv8P=D^c&3v7nBEu5#n$wtA46h4&`duWYC(jUE6>Tv&ftOcQ#pPzfkcab*K zY3ppCOM4l*y3?EemiBVrAWuJ5wvxk93mka~AF^z%&7eul z$*M-LdQ`*Ei9fY)5b*$YA$mHo_$Qen7MQWy)z|l{*ar@HbHAy{k*g#1 zcYWvQ>s#})w|`%S;0Q#2MDxHO9q%X=i06LCN%ZWmyt#)Km{l`@HB4phqiWyP3?e=+ z_e-mAGd|9W7gID?o5{v97WSW;R9`R2&d>d_Hl3>~w!z!}Np-InI*`t%iCRER4dv_M zjuKn~YSSTxU_#KuglJMS(E{sL((gLdeLA{{^J2$djR6cLt1*FYs*Dj(NsR@s{*W4X za*}h{S4sjP1JhJ47kaMPC{vF`h6buE@1cj{zgWg6|91HX7$cD`M6cA_5E2oFB2Qdz z*(}pBX9(cO(1+aTds{xWb6Z6CLs8~YZ;jq;ShBVPj&fmtgu@s90V^0B@fc3xFhrIb zA76zXUBXf%OzwKc5G$(j5r2Yv`B}(!{XzA4ziXGzJU7B&+UXt0un>FnHv;UADkYkZ z*@OshfEuH{Pj8F#xUt^CX4Do`5FKo})h^uVPISOvHA=ve!z<#$L$#N&@N+Ohd$}>t z-dlS)e}F^#BNw_hE$7X(f;|oY2jQU%j3&jQ^RhXxAB$v0!dnOGXC^xQLm6eB6-gcr zoa_}orPfIP5%7Hw|~cNo9?`ZVfe-F^!GG2LEBzp5!4-+Rje4aQd;j_g_4X23HFKib_!|2^Q4O+>a zciej?cix$oW4g6hTj_s)FL&t1BjI$9*a~b~%T#I0%ihXRaA}V;i>^jfZnL&bwsrJ} zeq62UzCE&^oKJB38V%_9yUM%vM7((8JD#z;3F1vqH#th;MDZqeEVE)&)$HMWC90Q1 z^$J(XdvAH~9WIvlBzaE?-y-jQIG2~Q0V zlJ|b{-Y=}lyZqa$!5rLz^2jc8Tan(mC57&tjfBo@G2g;dzwjNuJ;F{DJ2$JZpH) z`dOT97|&RqYk02bxs|7kr-tW#o)tWg@%)zO1)g<08+mr|+`1~xR>U)pr;29*&wV`g zJUY)KJWudE#q%uB3q1eed6lPyhedG*U8@#?>P#HX<+!n^lKTf3TaaN`tk2!cMc@}M z6d}Rqa)kJZvOdf_VT|u9%i(+k4WSv4z~UquNEG3S@5zN+GZKH)opf%ZTHJcG(TjTv z%{95+A+qI6%JPtG@K6@4&beohzf4E|;za)9MgB4!61ov%1*ZHZ$yqRuvkG;>rX{|(7yuNv$BQdyzO=JkVA+t~Dm zh-9?-dq^Y#B`08DreU*)$#$c>a94@`4p!;J#7`Ej*kQ_@)Y&3$u`=5+iI3Mw?cEIr zeK+c!YRM4+IOj67haLYpXPE3P#bS7nj=4dLwHpu%NVW|I0Pff?SVo}z+XCovk z%gWud@bd4IW95Hl!A!71c0qqZHZfV&{cOcpBA+wJvTFVV99YPy+KT+c(5NXeztjX5 z;;&&Gu5s%-003LoK$pqAl&~y`a2RV|Hyh?J>?Yp&O9LYW&EKOU_PV}P?DYiU^p;+b zBvP*2Pic@27VBWK4i<+>)1n(5x3>ro5fz#c3_sc>|7VRKkWcujC{GldlaWI*6orQo zBXV+n!}uMdJI=y33dBW$m0X>0n*@mpC&z6F$bknX;3Wd^jg|9a&q;cz0q;+l;SNLg z6&b1Xl*&JucfB48^GMwNAXdA%_rcWnHfF2^3Y$@~Sj8%tag0<#_8)|voU3%3EP67i zp00}uF5n+T=$Ja@T*fosaBS=qj_5?&s`$T7y3|+3N`UXfN+-LwaizTXXRXw}V6-vt z9HCiAHRQqqC7n@Bpm70Khv?*wM9TJp+2QbssjpH3%}u;Kf&GB&=C+q|8KnsaIUL% zue>zgW{l5}b%gKi#rDvB7>Nko9+&&k{p*24n1eA#U3&eo$4SP;X9FjFLSD5Rir5K_ zgl`&qPP{r}WKGl+Ow`OmLMqi!7&<`lYCX@`>3xk7({53Mw>Swbk>wrc{*gIVYe>xy z(}f05zo@V&({Oz$g9u7>K5+khg$1@oI8%2KMVMm8 zu~MPAEwb#V-*b@l1y0J=(*v>MDX1OZ?S} z_>1eTz+aT8rt{1)FG}I`8&(dXGV0`56`6Yx z1&FwX$z<^<91#V0hI)(NQ@8VQG@u}Y48fzL;Fa2b=jlj((DQjgg26m=EMNaPKRACi z=K&k@n*P;ms$Rdp@Q?Afsk-}c7MZ+({h`Z5{HDfa(DSU3k|022JSs&n%#YmnBG1p| zipC$L1aJCdUha2V;Bt{9=q>u~zf_P2dJe7VXDan_jL#`n`Q4_S6|LcZ>J0|wQ7z2! z7w8XdlJfKPlhZ)$yY2@w4ZzlQcctAKbMsGx@UEM_QuJq zCk|hIt@iq>6P*7OkIw2QkABFVeXyYTgM!>W+QakIoVSb5@6Y6DVDJ3w)*C_tzGjo7 zH}(@*Crw`A?te7~@^W`CIS6l!#^n9!63d#3tJ>sf-xWtsqiFz^sa%HA;j4;HM=%u9 zF7+^TVcGQc)slytMeLm)yom|Uvx75=%*yCp-n0Xa!9;{T-)v`;iA2-@tg^V0W zm+@_?De4aS`ux>QXWoR<9kP+Gz1&NzZD19Z2IfcuLpQ{UC3|@NhsER>r=}@P)UaVJ zJ|b_5+|Al6bkj$&uOS8MdyGY~aw_S8&cRbJalEgaO|B&Z3u8_rFb_V)QK@ zThcOTu&GA*#sWcPX{4zB=mpOLp>u@#a0iwQdh|gSawQd#Xf|v%LBQ=a9K@3WV)SKW z#5BsaUg3Gc8#08v(l(j=sL?r*^vlPJZq6>5?%~+MjQd`Mh!uQ3nH#u*xBqeihJdvP zW$I-raQ5}Pna$pCg(jz}tjZ|I=?P7a%LhBj5ObiB^p7uz<;W>AvHS0htGNy2inIvg zlc|zoeOr4S^W7o+rayKJU@akdjQnV{H~ScN5vt~%j;u($I?_B*|P)a4(Zlt?Q& zF6VR-fr4Ccl^o_5>G=yP#!?#->*H!F~)AKwbjkv%_9Jv1uApLB8(WnZ?u@>>a!= zDL;2ZZIx^yI=#8y)@H8g#Vs@w`}+GtuH$m5RQ7;Oi~@ z4>&2T?|>A_uFlrcY63h93GLXp>k@NYYG9JTt@&jblP8few79oukmwJoN=r}meIKeD zdNNa*2gBjuvMFvE;+h*yHg}n$yTBlWFH9Kx{@ecsg9;A){dX8>fd}dG7#uFuRwMqw z&J@~7{>+jMVslMu6;_*hcg*lhH|R*7uV zt+%cfV`FH4kK)XuZopIJjXTQ!aZymOQXe3PCReB2mzKcg0o&Z!*uJ@rjX;uSB%ojh z?!C#vFMCsP3f7uVcc6gB*z?mBbfd5|4`m8y1QYA_n&1{7f{9?^-Qcu&C+ZxqQRu0; zPbAz=x3k*ZosrL-CB=KiIBteH<49}!7|vo=sLyXHM`ltH?O5I?+D2+-mjF6 ze=buc$E6)ckyXNCRf5_}InOMi!$%^P)WTnJmJMdBK>s4I={xA<#Kx(5O2_0Cz7rEd z*G4ABE$oAB!(%(yp?VPjoXBy=O+6f-lljsJCkwNR57w;R=(EpU4U4%CCg~rO^He?2 zn4B}`c#anO5(pTtKPcRoc^}h+Cvr%rOn2VaCFumMzF8r)-J?QNU`=j1JXOy>krhcl zk|ju_)jvfFY>xWwbDrMrtP=&`i^*%;(d$E*SHe;3%*Qxf?!Rhp(Qkj7?blw~Y7W7B z4jVsbg`%x4I4s++mq9gxx4*5%${gY z!%qFf4w21y^zE=*6b85SwsF?C5t;dH`mCgmC(ITE00rO_5S+-l?*^y#`gW=HWo*6p z&gGDxe{uILes_{J)hpe@{&~+_i=NoGDQ(p|`3*%!FGX^?Bq}M&4WVeW|2ATb3ISgVK z#t@sS-<(s_l`H0Y&&gzZ`yn9#tPPm8nAGz_L$>yfV;2xv#-vA~6(}+=z6&%D$J%$f zLO+c2YMzsB{aZo~tBW#^qKCz%p+CjDsS(wRNXJbeNttKDj9&iv?iCtb4YK(~>^L6v z^%C)YupfSx_GmNIxC6aBn`=c=X8S~xF^Mn;99XK`yIlXM(hk<Cwezy! zVVi2+$s-jc9A30xEGII5dxeE4A`!b*1QM>taw!3PN3ZJlK6065{~2GKGNM^!+5<=~1CxFZZ^60U4kmyy zEo%=?J0kqI#+P}u`;pF{8uHPvRE$xk%^dwZ^?u3dU*c-@4dypHztKMXrcAr#%QEef z4cd<{QK9?NH<9LuHfQ7wSYjgP^4+*v{T>-{+RTv`{YDlzt= zgeFR^Y~_oQ8{~@zKQh~SG2030XRExzsmZt)jq@>9C}PVImo__v&895&8?u^+$R&56 zHDm_qa8m~>Ks-5w9%_@*B{vY$4(qR>uC#z zH-u-sL)2P(p-MZedBcfVKd+?V^m!_Lj=fsk@T{F1PTjGE@R8M4#g>~Tf4$+GS(_=d zZT*3c=SNOamF#$a^sN#bp53-#-|PdpyVUzzvkz|AH|x_4`{sNGkcZr?a+uRXSUc%b zUxtao9r?PrAJ4Wc|Hre3{962{X07J|G_zW|)Ks9lL!!fT(l?3QR=aEbhK_ydqr`I} zt9e7)$QJPG&`gAO114B=z@?D}4NjXyYVIPnUen;TzKzlGKlzssACVw{=gA zK8wAaXy1jrOqUnI&xO1wQU}flRH*VMOneMh;E1vP*|> z#@XT}fNcccvJhRY=V2*ES_o>9Icm?hF7HO9+#t5r<+ z2b`B+kz^j8xXPF&26fmEId1%qx=L=Val7o%v>NMx1I9P(qz$9LGflQFu&KP8Y`lSD zG__eZon+A}5(?4#gk`YSLah5_&4P$X8Yy)CrCh(Y7<5 zBggQ5<32XCR!Rw3OzUzV&{kssL9EbP#ik@m7Hu-6ig7&_5`7D>jE;MDLFl&lkn0Vu zxNwK^xZmOp&Yo|^7ndi3z@1L3J!wvB6K4T){#W^8^& z8r8+?M?_9uW(n`Jj8W7Kt^k288%9dC#N>b;G{bvqt6_oQQN$`3z+kV(JgZ_{4)=-F z)QiJOoTDw*_@)8x7IuF{36pz8nq7g`q`Lj6C%TbHwFWjPW+t)Krny@&Oui*PGfi*N z+-+`DNV#*G^}goba+6N1!*oeneP$gLP2bDwLz&BYT_+ld zK`;1anwf=pax%~M_Zs}0wI@I5+-a2brRH#84rA1dimU4l*du)uDH34~24iD3*pRhF z$Aav)9SM9qR6lXFDakj0mDFNwEi~9>JWPgxkB^0KQ5IXA^(WX3)Yg60r?#)*Q&FeF z6P-sl#bHsI;J&q*dGcqLiaQ;45EqY$DB_Z9&&HF6Wk}IfF|JT4zEeR1BtE4!b*)2U z2CJAuvP{q6g5`Y*HxXo8=qWH#mU3?PhURT%zvD0;X%|%k7kk42nUZAB;}CXz^{*Ak z*48mlv6GLwmEi^ydcj;_>zpFr5I~D)1`gDzm{G5>AKFEze<8@}4BYoXyq^QJZv@&u zCf@&Ipf!QL5V!7`E3zT4WR#`kUdXVku&ge?FO*P+F`dz8+>4bYR5r$Qrh2T<2q)@t zs!aBdac{5EZ@Np8k5#2-@gK&<_?F2$JeZRSZsuoM0XS7lj0U{s*9d+O$*SO$O{XJT z*55PR0Rnvjm z9gbR$q^Yx8VmvJU&s1~mAgQwg6_AnvmxFw2J?KA=?liQluhF!ImvV`I+9*!g$N9_M zLFXRr7Fe+U(|2-J7Ot~(le2Lc-*0j%Uf97$r5b1q^8D+ zQflX5@?|c{ry+}sUjltCYonHB?nlnE*3e03*o)k=LlL=u+l&04 z*bm2o8`rAsF#fFOy4_5*3n@cQwL7{_wHvu-o(rFSDNw+oV?d7CT**l~XS!|M0(lwg zgdy8auB0G(96{G$o~&iA0p{A-NgIxRF0we9b=p5h8a&n=nv#Iaz)zj=2Q?+Q1&!Cy z9JY!cPZzYdH}RE#LVL7@o((9onWpZ5p8AZ6JMrB@EMxyjlFMWxxF4JaRh~yRs?~)) zPH&)a58}k*1$wJ~glWdhZino7rxud~_iga4z6ZekFp~aLKomIM%RiS`PnZ1rtNhL7 ze(KZDlc)qKgli2Ovoe`=t@(!{87A7nf|89+h=%>tXTrLn;rmV`eKpZy<+dX8%oha4 z$V!*>MBocl)dy$liJ7!9N$sPX>hnrpel4mJ>u0y zxAJe4Is*c6utXO!Qcdd1TeV3>n8pQ^UTB|!rGle3xX9sUtIRSP>p=0srGGtc*Bi*; z8-qVcL|m5^oStMlDI5(*6H|Mhv^O3mKHyD^BPiYq`k5INuZ|~}seWn(B@&cqrTU2( zltfUH6|~X}N`q|kBp_|*&54m?JIA@u>rcDXMe)&yKAi2aB9gjA^yP$y6_MC2A~~Va z3XdmT3<#4~laXQt#t~?I&KSf1Dh!$`Dv+0wcCtukjs7V%z`x6nvaD6bvaxk8UwBIL6`d2Ykm!4bou?@1-`uZ zmG^k_Jz3td04(7tdR(AA!8casx1sW{pUiPt-lfU5LRjdbk|vdZ+5O<&dG9Chseo?j zh{$gs!vYn`oF42Y;B-y*278$tIh>PIj(jITxdK5V1?ISCXYXJG#u#`^>c-xub< z(;MpU1))7%lyq63m^?Yl664pTrgF(GkSbbwTgR^mP&C#ENEAotgs?_NZs@PHuUK~! zyC%w6`h=!@eHY6O=KznkdKSB%jTu}mCw76(*QclC=kBWMt9z3Yw&Y_GpdXv!B*^mQ zZml`MY9&VIy3c>`(4k2WT_gCr!x&GSSZNHGduh~>7K7~>l<69Mn(oYOGKL=8$>FSj zpO4lQ+{#*ZLY&{>rKaA5rlmR4JxrMW*CxkHgg3+AO9?4k%!)``u(hHV>L0DW zwuSmgR9?cChF$rZXLBAEk#v2diHY95la<|DdegBzBbp+uBX;sxXL^nSy0q7Axy=Qv zt6X(CHs2ShHbZ4K|L4?JDMGuntBP&T+SL5w%>@aq$_UI{GwTn_0nKY!H)Vkz<&6WL_4}a6JA@!j5@v~{N05HjH zmnhGy=tqxr`)F-%$}w}huX_YAu_8*kMU2~QCcnO0Xx)9)oI77DJe)C}m@`zNfZ31B zPo$4eV+o~e#TyWq*`gKv_MWbhYMF0-^?vsVmY!AxTe?L6(-?%lZtj|wUNGIv>Ah~j zbjDaSysSsP;!M==r7X=E0*d(9#_F?mE~S+xem)sv)t3VncqRn-DA0}mivO`uCmYdD+&@_ED$Bj!a;q)(4$IBA+^a12V#^(FxdSY> zm*t)en&t1Z+;x`wTgzQxx%XJ^4=wjb%XM4sSj#=%a{F5DKbM>Ne`vX_mivO`K4Q7| zTka1nH{Wu{TJB)WjknydEIim~xqq_UD6&+UFLfdk;8*9fTyB$+U!Bb7;>uQ`xGX}I zD`HuGb*zk(d$#4m94bG2hjK;a#jlPvhjI~dDOW~4zd9Mn;+n0u)rtH<{Fo$H?kcOk zLO%G_{n+v^u-ti;JHd))%_qOQ6w8&t%dbvWNaCvh{5u^mOU$>v&#~MKEO&tA+AQ~r zA6xxnx!W!GRm=U8<^I}oAF&~mC;smpmv_$fZ zc`kO`RppBoFsj1*B3e#OX>n=!f>H|d`ATalt?Uk8W4^RcG2O#&RF*FE)hxEzs%xrz z6k(e?cYdj_$X9e{MXAkKRZc6+-(7^3lu)%YlOp)4=26wkqWPthn#1n&n?P$MnD5BmI-_n%~!DoAK{k z@h{CX{q2_jaJlLK#PaXVH~m&MVonrmB%pLracMOWyVGA*R!XIP#nm={NwuxEv_iTR zCBx2soIBTFSy5F)i_PEMn>{zq$h+bCs6XFdG^48c?oyv)+?cVWYl<(mBK(zt5`wVh zmF2#2KzmQAqoVxInj*m->4#c>b#+w@Nb0Dn_LWyvItmr;g2`0?`p&AViqfJ=$Awju z7dpzyODjrZc&tjO^v}Puw8n9v)erxcro6g)dhn~3dQz05tf;&~x={V*`zxrOg$#t4 z!^v@BEd1YcHouFGo305dQ*tVm0bO3nFe)o5w$MG6Z!G-3<=Z7;*Lgn{~FlCx&X5RHT zyYlng#LvBpktlAIBr%?fEj)wNNt_xx^1`pUs17j?T-2@%4>b47Adpn8*nk}LORuM+-~LJ?^d4V*R?$S(V?$uNPW8bYv=iW zB|>1V-`%C9)gb?ZC?3?5`fE$O;G6QxIIz+)7DrcBm6TQ#nFLGxl1F(71eRaaUtDQ^ z5%q%)3xp_EFd4|0j?yFAy~ z*uC?MfNRZtF@JGId1_w@GY z`K3axYUjlWE8!(IRb<{HLc!l(S+2^PS5+NFY!%*FlUPiT@L0(z{0t?l@H3PQ{a1aB ztNI%EUTch&me70R_g8ieF##mu(f*Z?n$j{5yu=1b9aUuxnpdR={wNEcCjGfrCI^KR zbKO|px$Z_+L095pYjmplU*`O>D!+_ai?BM1Dwx|#7Bj0jC?`foWd4X2txAffi`CdF zzq+($etB)Jq%SG0ERW3tc|xyi{M9lamC;Z&BNZ1_L_2tLQAsCpC$Bte5Oku8LB#2X8E`t@76tGb5JHXSf&Dz>zqZBo{ClDdx$_;07vKqzZ>k zkygTJ-DOTGDn(~}w1wT%%ur;TDK(l64Xv!IG^sf`xJzq&<&|a$QJB`0{>WcmV}hc9 z*{kR-nYgPQ6;+jYMcXwUey^&ys=~3bhERuZadjzOQ)5jN7SbpZLLEE}zEBT%=W{Sx{!bX?hC8+WvKY*aGEg0CYFZC^~ zs=3=yUh7cgyNlmn3Cb1EGkYl-E<>oA?=Acid{QXL{GjSTqpHleu&AaK)>Xzs*K|d9 zc9MJB^;Hg&Iy-8s;T9Y{5yI+EswFM$#Ad5KGMzb!%D`Q#CmcN{Ag}Pz(MC}-%2VV{ zI5Qbz(f*C$EuB~l4p?X#gG;Ud&f3L5Y`$c62E4tcT;bT5l8Ook#9s|Ciw4vbF(pOI znc;^isI4vYS2%8&;hNzqs`2@&RofQBLxQy+)qGI#x*DJ%siN)aF&3_`sx0!eWT+`; zwwHxV4}t;CkA~(~Q5VIC3e)GOI~Xm(JAeVTRTX|2XlBiBR;w+Ex|rHHN?6j6rnssG z_Oz@Ez6ip~7dM&fh#21=9oCB#YxVslMdveCcYbpO3C30w-DMGalVC#TDyoXjQEJuP z0-$Q9j63Goa_~j4f-YN7UQ$|ev4aQ+Fqb&$i>~Qn!>`N8RU_HTyRLqW%$w?0{o8o8 zE>8QM=`yKy`?-JGeVyS5o9h)zK4Ked`7Jq%EvHAk*YfwV%9~~REqRV@UJw5w%b#MU zU*6;URhHk9%h;ak5&u#Tf1~A(*00s_N6X)1`O%42KcmO@+urHEzW=8cpJ=7GePPB& z^G~w;mORRqV);#J2)9}F@DJ#bKBGtc;2wTQ4}X^BkM_s-9`Vi(&H6>_lhY%9Vh_L5 z@<-FVd-%OQ{P~vOlE>Mm_lTd_!#}Hse~#sk!l$rDd`XY^c|GDQdc;@vi1+pIFY4i6 z(&PKO9{&0s{^dRV5B2b`?BQS4!~eMDkHYK89{#6#q<^}H|G6Iimn?sDd^Ps)@3j0; z{M=*tqxfQ2{sB>Z{<8b{PP6=HS@Btxf3W5ETK*xHe_oIHk;2-`A1maAGZAG zS^l)I%=$Pie~#rxLs9)^S^f(wf1Txze*cu^M_)qyUb6gA`2XGV+pYM<9_e>l{%HOF zY5Ak{$9Bl9UljgnmcO5spTqJ;>*KWiQTP>F{*hMt<(5Cw^1sw0evjqPw&Ifxo8?FI z&$9duD}K7=A8z>Uy&T&k zz1Q+b^Pgw=qxe;4`J?c8%JQFUR>#&e|DN>CYvFU0e)SyRJ?TMD`ed{4FFHONt@5q? z0puQc7QyF5!H2)Nq_nsK7tt;22&}##BtA7DvlB$|$ z2>Dh-J#*)l7TsBH=1jf|%gcOoqw=??|1zs!>fx)oVm#%;`5-bDIiLJQz6a}bSEaQQ zipEMq90+?=B6&w7QOYWL67N@EZL>|BXtNFDeG&n~cy7DhOldAWOi}1)(q)5?qet@) zEx0M;arD)rY?t5)Zj+5|BHxqLb~WzBxLFt3Z1>~d!Lx+t0zO*8%P8Wrc}DYG!ZVWR zYCf7s1{d(Ygy#+(`4uiGQ$pzrO)vgCd-w&S7upJip{pv3CbJa^-v!qxTxGH2E`L!? ziHa-iA!jE)`WvVa?w1a-aws&r%LEs)n4T#LOG?X%kU0t~AN|xJf-zV|$)Zqjp{VBj zzqj)5`gu9*f00#%&hlfqcZ~)P3Y{Eg^}jC{fgb`vKaeOXudS|t7nemYlELa|J4@%m z=S#!8wWM2FsvkPjb;8QgnXIrg{y$c!u(+yvakrwn08QZLFY&>q`}{SPYCELRr({|3~a>COmLDX$Z*{|32E=yyvOg?|q?cTG+DlEo~N7#f{o{Tj8L zh~h;wzQ|X)sNB~D{kx_jpBVD|$Kqn?{#TWgcK;h3|4aVe#=CExNSC?{ds~#AIff5+ zkbS4WxN2_E!lLr%0y>&*;v}JD(94-3Awk+wTvJ}%NgiyD$wD@!$gW?PSVxi1VNM-g z<6Sj1MT;%CTAz5!8R|8~^A>mcSfy}GshVFc6xwlvxnr0w>&F;bvK1DwB3?Yd%8wv{ zMXnMy$cKlmd2H+vbN;Tj+#*}GtsJV2w3Pn~d9TDRwpH0`Y^A&*2}MR*icjJzY?t!i zd{pyo|Lyx)+hTGow#|!ueHm}jT{oNUZk2Z>|0`_cY|&W^xly-znPtfu;o6E?m#mr1 zkZ$4E)|9FxyUj{>U8#@NzTXU!lvenRuHnKbNO)ds_i75F2E?)zGoX5i4LpnF)7THb zua}LF(rgL6Y`x;s5P7sLdK22)mXrja!MjcVfne|+QVIW1pbR6ImQm;B?q@%Ht*mX$qnB0aal zt-@?+dsmjOx_I)PyW>2=e{s$Q7vAt2$teEO-c7-qGY3C=a>HHoTWjB+|L1T`oP5x< zz2w5*7H`RT`q0RG+up1`^wHfzuRi#zxzgb_+b5N$Ti*O(%By9U4V^G1A>MQA$m6?y zbo12amiMTC+QVa>OPcc6KP)+K*iW3>SA3JaeSQ6f$>Aj}t497gPdd5l;XC3d)L5M+irYu^{IuM zx$oEY{ln?Q&b>c*ZfD(jcaJD$KQSl&vd1>v5_o6w)K7|rKfiZU+QiptzWMa0zVz9R zslOlcMaAAxX>WZ-nQ0B@-&S-(W%;OkGX@`+o|rxUgG;`;aC-3*Kid5JfgMwSk~Jpj zoT|#a^D563Xge>v!1?FI={4t{c&BaEMX$VhTiIh{a(}z;zLTYYn(^VnU;ftd(u@;t zZ2aTMRnrPSPP8o@-E!)mJBQymJxg2t&5^TjIeWn5_wT!X!n;qLJNx+HoJVZ$4*1)T z*DieKo}$zLAA4^C7RAx_dsnlsG6*Q(hC3?gsHnKm&EkRzihznsbl4P>Wn@t_Mn_{5 zH7XhtjazUHaV16*G{)eLiBa4VT%zL=C5cG}M8*BPtLm;{Am%*pd!6UKo^!76v^M|# z)m?j4SNBwP&wN#XQY^IgUFRlCCLSB%)of+2#ocY!+9YegIh;7v>-Xbr)~+v~=$2;d zw!TAUmnkm8LJyza^h>|U+4R$u>g$}9Pt1z58of{6G8AT?y!M&vfh)IfW;i8=4fdYa zK>s-P=<<+Z=T3jvcKD9lKeoMBux{Ju>?SpEQ0apzLvG92CsOTV(k;FXIrf%sb*PQ= zpN&W5Kl3(J{h(~^=#f9=$3qMHCch)4JyoqA;W<5f%Q8Z4?LE*ZJ9q8}4U``y_u9Ac z#)1tt{riO{Ic&T3a`^|XZn`{3Qn&59@8ds%=6reVJF}RIVM$juwVwLUk|{%=2PHhe z_wMLQr(GjgI`7w!3H#f5ox5=2r*~47t&PLK`r(Fl{ndb=244;Nu5`+3m%BgZ^$Hw% zTiNk6eOh>%dwc9?FH?0MzQguHjKj9?+r*Ao@NVNtN2(TNPc853b8bL>&<0DJ)3VjU z{PL1>_lFlhyS2Bk^jG05uU46@GtSR>T6A}}rJHqNT<;k}-`aAvYtZeXsi{uQE4z;P zWrWL-)~kN{-Fx2eA6=Z*dtlSuzRd=B`S1GZ{Ml!VuKjRh$o%4@;Tzvd-n+0*{Z>=@ zy#vEfP~*0P8m5ih9rs}S!WMu1{Cv}wcmKN8u=1M;U3IzpddNOS-!8wg)59jA>e7%& zue;ToG-9Ibfop-^^=_Y&w6@FSD<}3mI+UCMpDf7Ts`0w#d(IF4qrhv>+Uk+3cG$hz zExo@9Bp$8o@A+-^?KnP z`pd4R@uO7tzh70J{8Jk_=-G8$w)Kp`%YPl zS30L0YaP7!>FM>$?KAhxd~NxjZ%AQsb_ZXdc_X)}{%TfA0%x51C9bG^Qf`^*m(@R% zoEW}PHT=s_rI!vrkLf(O$lRk}n0e3B;T}85iDtKp!=`W4o$h_!_w!$?TcnO#v(W$T zGLPwJGp3%*`Q*Sa(}$R^yvv3TLN@+-X>3aRJ9pRKzta2Z#`L&p!~7PXEFab3M$C;q z=VD&>Z{ap`WWlKH`)#5oPHPzd)xF=x+r7M#+_SuEw|vWj1qV7m4smMM{94H9vX34$ z+Of$)NrK^#B)od(a3_n?gA4z7bnEJnjmt8x+1_3E=g^6cpSXSYwV;(x%}wB z-_NY-F#bcu-|6ue%e`EC0MupC`qwjzy3pV zBy>!`qQir#x?hP5FTXbBMDxZ^ZRhkGG^uR+Pcy@A*{s{THk;)3x!aJ&O#)Y3QdL+S z&PZ@~IMRK=^Bp;^?e+|N(R0=Bt=-fe!tCprZ|vRYmG{9Hxzl1oHWY2S8<2Ulvj5E0 zaaqfpntNQ^{pIG4dtD>8FWJ%aw_v}IR(*E!ru6FK!wWjI>G?e%c4_Uph>Bg;PxTgyBYll@=njhbED>A=VOVv-GWxm(<;FbF4%-t7g*Y>&|SsuCfmm$v^ zT6fwOe)!f;JM4!35%>Pv4-TAd9ys9=dDm^@z3e+@D%*9MH-~2bZo_E_&mUc!5->IY z*G{hQObDy$+OeYO;ZjE@m*kaUnFUU5G=VNsAG_QsXPWzN?eW*?tBdw^{yI1)?CFmw zmW$yFS}bqR{9{$*@z`T=+sm#lxba)pqSz$E%5T=3ahY`a$P)PZKiE_*<=tnT+|aH; zm7Oy1;kj*Fr)_R|c+1=|o6dB~P<5N#cm3fam!kzkx`dtH)pf(6gRgc@?b1A~W0xOX z~*vDuYT@-c1bIjw!1F0{L15Ni{l;6 zSbg=~@1GC2KG=5scV^d5EFE^Qvu?z(r3Epa{DPZWEO!0n+gC0xURyd%kDvIm`{fB! zBW@jtqc=aXg%dsPqzlEjEK7T5*1x87{ABa(^?PG)UypBp$fC{4wU@QAAGN-7^gy<~ zYjd9_%`}r-iZ=DWurTwT3eC-*zB}3BX0la}1x2m?u=-%a?`Z=)(yuM~AbW1sxbADx zPIUe%ultE$?~&+O&tRsPY#$qaBj$F_oolK_p-$r z?T`Au&V6V3)0QRQq>zL=UtAxuHO;1A&&HNMZszDS4N&0v%lM~ zuUt}MhaLC4cJRaf>xWcqjJW^w`jpx6)*U|SbYp!B+FnGp@SAacM)a6=$A9zp0Yf)j znB%kI$(F2BPmis--*}S$-SIouEq-YI>_P73okvE?XV2_;PpdRre=*g!+`;{whUqJl z$KCQ-nh@G^3J4fm~l9NPAqk!@ynyt+6$KTp@&<;!cnd7cVcGMl$cq!CT2Fi#N4JEF}Ib8xm_r+u!|%X_HPjj zhh$>ukWDNd=Mu|$dBoD`6Jq7Gn^>t16D#L)#M)U;tm{`2YZpt&#>HK-ar2dI8iYu; z4Ms_}4YiW3d#+^Xo-f&X9FXihFG_Ze{*>$+*_zomZf$1YgpMN#u$RM?OVyT;{P_|I z40a;AkKob$TaPU1kEhZiq_R&qrk^^-!l#oAk_De{#=@O9KOW0`#e<9ZB*fjvGxsd~ z%n}QCuj3(H77L$Fa!5z`oKt%n4?aX#Q=h#^FZgVg0-v*DAw2!mH4%Jjf$1?kZ%L+t zB?WSn2tIqkaVEII^A_L|2kzoi4b9P;Ej%Cp-t=p7EK1oNa`nH;fkY&U4l%HUnjFI+ z-O%;Fcg&*We`vgz7Z!(zb0GYL5Il5B1`Zh*O+RbUX$<cAm=+ri^&?Vw*5)6WB9gZuendhHY2M|F4!F#Y@_wxx9#?&50M^Xf2sZmDHoQHMXM z!~4u?``=%O#b+1(F@b+pqIsjA??qougnpU;eJBOoL((()!Tb$8Zb6sp;q#^kpC1tA z$*RMC3spj!Cq*9IN0SI;-Qx$8dtV>LJQ)AM~vVZQfhz=;!mdp zX_z|G^o$%7PDljobPEsfl#tMAI*XTb0&G5B;jnqpgXyqQXA&CPD>bzjdnJW&6x>&& z!BIHeVNi``cOHa{ggXs-Ulk2^Hrb&4;a(>hG>YA~P^IA#fOxz-d=2w>GL1fXe009I z#tovw_~|TjO<3yA)2ngg>1IPNYTT&5L68HQ@BR>T9B4Q&t-~O=r=m)k=5P>}lPbOb z1%XZkje>Am(4laZq;vg4pdO}z_62v^vONB57AuX1r;`DpX*#^@;Ed_g`EI`P^pjay z)P<)n?%p`3n1tu~$%cC1D-Jwg^o}zX=izz0JfG=CH=f_RzC`|Oy9Yro=@x-XkHg_# zC#cf|_&ePsKh%Z(^C4c$5B29|bfG@E?@$s7cd4oHuNUMUA990u6QNan$#AIO|F_W^ z@cP?7ZF>Tj^TXM5rROhPyma}; zpUSRWy>|WQ8#nd0Zr{24%f0*Mzy9|7gFpWK%TQ7Iuo#q@+qLiD`Oofw6J$z{`Y*Ny(EYr=+H(Yo}ynW@S&EmNR|En6Yn-8?X6i``a_$nKk=A zUH<=c`v2SY9~2zYw_pDOp#ujE4htVLG-6m}R5bjiQOu~(y#D{${(naOtN49g9%?>n zA?hT!p8CR7QcO9`U@ONVa6RVRWVkN-lEG|Uk-BhuG~B1~P23dA#mGKMr4*peqe2$LK$C-u~Ir`V(r$fp#`~=9C zh=H~OueE;_p7#hkA1T7p+>6xzb^GP8THt+%#=ujqiQ&!Z)AYpbkT2vN-gkz0=u;o! znGibjIPDF2dUJdFe2OmnUx|;m0WUGlGoPWQIsJQ^(4IzTYUxunrq8d? z9Ev$Mkv7#g*B5owLK)K`41E&D7ix~y(VIpA-jhZ9h%{@L3+BKv(V&dP{_t{IkTy9p zXh_sRxZC5le^PjSfQ6;=hcu3`knmV-6#MQ0_9TqaojyL9I0POsicSv;p>GEBXG4Ot z+KBX&Y2AzN@HxyUfz%(m6Ybm@w2`N*_A=vII%=9&wU zZMm5gA+z;Va4SX@bJ-=xVt%+3*%8B+A=g9JBh%-A=vIy_`jr9MnVU&9a(!gdhS!G+ zvJJ8;vIddk1U=m4@9QV645ObnLnQcpM8;= zax;lR7Vi~kkiF2Jgxnlii_DkfAb$;cd0V1A2kotpb;zxebCKzT33SUthNUaEtwt6T zyZOlN(O!Vu0l5&_2e}B@7r7X@6LJZ1cjQuJx~5IHGGu>_M33APxg4241qsg=BI6f{ zkZR;W^iSII`Uyg|K@LGyA=7h|4#r?4*auM3C zkV}xQk;{;6kjs&6k;QzfJ#sbL9guCjd3!k`yCc^__C|I>_D5DBhax*8>+t@*KC-y) zcR`Lp|E|bM$Zp6v$PJKlk=>D3BYPkhAbTPgAvZ!UL2is(hTH_X9JwiSHF7g#n|8c@ zypY|InEKn_IqM~*=5iL62Hg{(#HjjThKk@JuhtGi#pii= zkeO}XXtzc7N47%_MRq`rK~^CrA-f>wAiE;xA~!%@jqHhBfZPPR7}*E86uCEYIkJpg zjU0k(4Epk4x9de-vk6bLmx8mVTMR;Vr2#;(K;gN|N&u4EQUWIIn z?1gNHtQP)#xc@-mA2~w!N7e}cYVKbv{3Gjxf8;#j-;evx7ygk8g@5E?;oqP8FBNuV zy|DM;c7w1Zlg?QFKyFtd+aY@i?#u0J!J(W31&48t5FEi-gKROAvliJCS%=J5DGAJ5 zOk^JE+#B7*yk{t`oW;W|3Ek-YAXk2;NoNb_M%UT6a({FlkZu!TCV_5A;Dak&#f9gg zFg`jzLpM6_N;f*cO1CtOHw{^f4Bw*5>~z+RZgif4Zgk#_ZkhPGIFn34rmLv%eOBOu zEB8m|hUk_IC88UB$B1s3#`5rVQrIdieNTk*1kC4DEMF4LE72_$YK(4)n4eV4M=aC; z-6mr=Ix9!FBn(gImFPAZ{ZGdDGGVrhZrK=b7N)PocqU>xbasetbY_cgld&CAjrGCp zspvit%SY>ZGWe(SVzegc92T!B8VfBCoqLOd^=ZD*{^3i4iTM0O`-v~=% zR(APD`xCWC8JW)74TN=lzR~{V3$yqld>XVkW%2oo_Ag(U^%r*9&uDo?`n13K0tx@L z-%%Fn)BZ>EBmC2TNb66OpY}&zs7(=nCe${yi~6Ab(-(4!r#GZd~FXT>q-lYAVvZz1W?`e9X ze6;`jf^YG8mYxT+eE1j$_@2RdH2e(%$})}*wBBm^4LwhMA(SW=_2&z{Nc6iLRxdR? zgVlQt)AP&JKRwTUp_hts@mcWx#&)CU9W95*cPcAqxUrmc=Ge4f@Oj^GV?EOIl&=4X zeooI<%Hq7B=PhNCK0SYFyVR`*dOn->pEy?Tntajo+%%u`d^cr!-q&V+eu#3>alq7` z%G$psemXvw+B56KN5>1=E+W2oR?Y#?qxeS05z}_$<4H}7d|aVDZWIf^#}^tS?_pD* zJhWzlv1Rxyd`-L3=cH*nhk&w143kFLki)MC&-A5Vgf?Lx0R zbi5Mh8y_EP>}jn0rsD(kA7Ro@X#GWjOYQ#6&+D3T%=A16VPkCy%MtHqHRnI?XTiq$ zHtjd2;Y7XlgS^v?*GoTR|Kr!~P~-W;>nX^XZ(dJ9#&L_+Q_cCv>nX%|{$;X$!pmuz z51#*;a*BLKU=8zpg&EHqp06llK6$>vO!7hdTTQw=UjvN!7WoqG#mC!VlYFMaJw(l! z%hT^`>_0sHaN{^2(ytj0Y5Fz&ji(=C9PfDgHTk7$hqU(w!C%wyEfZEzXg);C@_d9F z^U3Rfps^qFbRvv)o=!NnE*-aNKYMdJbOaDRl&2GI>_0r6n)8XL)7Myj(|BvQYbqNF zYUh6_oJoA+@eVR!S`Q8&lPNJ zS5bdryyfW)GM1C47j5iE+<%a>oUSF>2EKpz%c+uiKnMP5J?~4>7s!Q;sl}zve!l zU+HPTFx|wwu^9KoJiQNkDpmOB7=E{1vkoQV(4)N!9Sq1BTB9sVqMf7?I~y%>!xjxz0sb8EY`*Ckp0mfhAh@SJ0gdo zJrh~1quV3LpnWBB67p8$9OT`|xya)Bz8d*Uv=<;BLKf?gV%@a}?YU_8#`CcUatYeS zI;2?VcR+g?+QsJ#v98+*?d53CLiWe;(H*%O?R$`I`tkM=pBuz;1deETNBdA@v2N{+ z?2YzG$XZOV8L~gxW0A#k3H6Xe(T>w_L_DY9g!UM;e~6rfJPcVp$Dl&aLA&_eU55Gb zMSCvV#Z|i+?VZuS8tpmAf!IF1kPFa07nxvqXXGUGuOJtp|AojU$WbCZ`tO2VhIT$n z2EXfs@wY~MIoc;8=b^n7ay8odEE;@QOn=_KOVKW#Bj|waj`mb!Z{&B8V=#XK$o^=b zj~t5j-h$CS582R$r>{niLHl~-BxF8&2hZUkXQMqAc{s978y;VMY%-yCc7k?2Y^h zvOjWxuw(oUkVDZPgRDk-L*y8=^H~LWj;1Y7zYp4z(Ec8B4)UkSxyVzIS0k@O4#f04 zkPFa023d#p7RW_tPe3j~K8#$3dWu$*CG3(y(e-x+UFq0V164TSEGF#vduuAzcgeG`fq~lj`s0_F}-fc-e{jG z7@xcHLiR^{9C8xI-xN6%?HXhi#_xw5gZ2%`TI9Ep3oyR6$hl}wMqZ7aF8m{#BNrjx zMlM0lLoP$!h+K}m7r7dFJF?9nUZ4Gu-I2A(e14Wb$;~(Zc{dh5x0-Ss%<`IY0 z%3`)#q(9xHymZ!+&kDwy%m;E8{x=2eA_lrHUv06vht?2v%ecqdId|oOJ zVxZ3o*TlouYv}9%-}rh>ys@5XUVPc}%iPY_ZxUd(TQJQBjn9;8-ZSL$Y4oqDoj%21 zo5gid%m=69suN!inrNJT;PW=IFiXrg&T+>4au%zVVqPfT*v{NepT{;0pKQz@w@)&* zGv~BA@oSC!lH0{9oJfy81#KFCO+V-UXBhh-pO>M%P57ttF!X70Q;vr=tIc)owZqeA z#7+J4cSWfSPp_uEIZrgUALk@vzu}w;&#s%smu74~ZWpVMB7Ut&ebVyqcU^1J*BbjR z4^L;uP2$P)s=$-Xh=|Uk|CtA78JjIWPJ8WzBoId>zWvPUENNE#LV3 zwpi7s>&3ME{2gfOgRi&68v8F_4-~7}e0`mc3!=XGdPmLq%2}+w^YwRMn7J0?HeW}Z zXsj>3?lswXe(?3Bn*59PAK^mT7e)_Je|(*6vT+>e>x*JlovyFb^+&PV&c7#zz6m1k z8~8e*SXHNXy8a|mqfEy^`j>BfeaqC&&$k*on3uz31_KaXJAfB1Tl zcuIlVY5J!AY5X{{MM#Vog2fpxSe%i3 zy;?ja!Jm`hZ*}nU@%6r%{P6Xhn)8QqO?f!ezal(c2Nh3Si1lUq=8>@Tbp#Ouo!6(= zBoTvHCo#3tbrVxNtx=I9v93_VbUmHEbtBRf=MVk%So%x@xA1k2zh4izdnVNToBVTk z)9WW+*AuDJ^3wh)a!Z-6`_k`q68WKdpg#DOo3Gc_)Hi?Q%#`W+zj#W8KF>i*U(0CE58?}bM2sK&c>%5Q`ptU}|7%)5rtYTY+m)C;p+x; zJmDiu4f8Wdun5mN$(SF`^o=9Fan3Z351g}%*H6CgUz0utLovLr!?SC1bsf8n(fnH6 z7^911wD1G|V>C{+T@7`(COT5@mo{#1e}7Fe4Ul8P-9DV+-Q%+Y*~A{<()Xg#e``7J zhVqxWshxTaH1Q9I;`dU8tKJyi_(G5{G1b)A8Q1t9&bG^S?4?Ft^D*5{8=3n*VPqCv z^|E57@aLtqI5O43+WyI(b-1DqtBis1$AD%FhkMa92m6iwFP%IH`CDk@Pli=kw;UK} zWW(#XO8j(vj7;O9afYW~!fZ!-<|n?p6i zX6-zx#Zz7_psLFox0tG7($`C==D%yVoT`5R{uNXshK^rJl|)&8M78wTu2obGTOvQE z>h<)|8mii3tJYD?xANaWweZA`pD>mV=2P{$wO}LFVzc3!m`-fInQET%A53*p(H6>j z|G8VKstyd^Mm6wV*H5VyufEQ-^x5VD#@i=-Mz!=a^>(U|52j?z!5x(Imdx5oRdqRN z7uEbJ7Q3lx6HYQM+`V8ASKJMqf~OeL)UW^~K9f^Y?VwN8L;1WlX*1{LWN8Z2;>Jn&z9C2I^jMT{-GY>fi9z zKBk2~*ZYbxaZP5bk&ZD{Z);q{+|8#lRlA>K8hEem0qUNYs$-hJyNoH+&q3;*KR%bK zw)7^`!WZ4YrtYP8moQZ?yvsE3w_b;+yVhnoQ`OyjO!MFA^9^;^U0%+VJh;a+|48q{ z)V**-9@G3CcbOI!`+v*)fAl_6wXdFO{!x!&7XQ!?rkZb3nCcupVoCzOWvUul&eRa> ze1!VXckjhiR~pAOZ_;9>s{3CsRrkNnIoIMS^{?OQ!?bi?G*iPTGnwkrwlFR9I>R*Y z%ZE&%9gop)UQ2_ymL@aRJ}G~xUD`LkJQ`znMS-nj%i-ZJf?y6+nH*&oa5Z;5!2E|o+oK~5oHR~QinvQI`2hX zJM3Yqce}*x_p6y|H#RxN>LWOqY2cX&O!EU4GgYtJ!&G(UBGU--YNlQd8kf*;dCq}M ziyy@^)gN8JluX~j6#5_2{QOF$+WHOH_*Dr^mMa2GS##j#x(zMCR45a5mQ4!5mT>MSDC8imt2#ZeNV#&&JJR#)x|S4q`b$p zuR}FNXt;=H4co4bIP*sxup%rQzW`VjA&s3{y>;nN0P4)^QCy z#8lViI@40;7u@|uqjNNT>Bqg9<_C{qs=G3Ssdn5Nrg`TNFg0|!!Zd$wHPZ;aeJM+S zY9ppe1#pEHW7;Z71$ZT}pmy8H!9Rh1txRrlV;)bRFxrV;y&Gu7O^ z$n6eym=<@aWU300&eQY*BUM~SHDjud>By7}4PaUt5XLmW)flE)^GQr2uI4Ze{A>YJ zjrJp^`JJ{hh4F!D@kd9Q7Ru+D8gATVs-5;HQ;p3_ruob5FVOr%xOg(vF7alnB0ZR@ zC--AodNztF=@7?xww7t&53`tRT$V8Pid@Syf8l3L^#{JZyAel&SUPV~?!{{1kU9 z!1Yjgvon&F{OvD~hv^^w7V!H+&!+Y>Y~@pC#oC3Nivxz8f|Zz#D!E?T4}X#|Zt|Y} z=Gu1iZw91p?*7Grkt%uUZ&rg!2D`|+4sG0UHKc*u{?7n27K{dFI(FT3;ENx+TLHLZ6e=4FlE|?o6Tg6mEEV`wzZLYJ{rhl#!XT^?qMYl zj+hk)vd@m=hkVRB$bA!key6nI(|`><%tvYh-DFLp87mi`caUd&HnVNwu7>hw<3BkO zp6e{nEw$1dcWx+G+jQ>J(YBM^>sE(5CtMrKYlddN{PgQK^1Ji&`7nJBPXp3oPTCwg(^OtPDDvsAZq4PQ&YedEHtZyK^G|5K_*^4d_s8NU1>>8^ zcdxX28a}9%ywagXnb+$2vh5v<4?^wR%dZl*?HCc>OpYJc-6PMTx!k?&s8=~o)dAl% zE@_<`sgfsUrM?sOp+11ukGtIGgMJSR3a#YNu03j0Jz6EZT^`=%+3kk%w|94c`(Tu# zyje-Q+G%kUxkry)FMlp-C0o?r9Xo5Ti~NbxuLIwH-$!mTYL%Pew{~*(w>zh8+wLI` zx5}^`k-g@+fjDt7Uz-IvYk9`*?q#IiU-TO$`hted$eIifLxTn#Lzpm zr`#rH`=9lG?kykeVK(aL<2~h5-~F!t^`WP{)3NQ`sI%SVg9qA2Wex2qN6*jwu&tl9 zY#4mUo{VB8p6($rn$i0sAm@+l`Y`RV;`g|eKr^1T)%Q+vsG=HG}sSua4|6Ek0bW=kh|gI)23 zP4jxo=f@naEZpiPfBbk%3(b!Kau37W_nbbrk#BvUx4&hsEInLwM&~sqMhd%P{7p$ zH$??p_PW(g9#Oe4c6MYh*=gOS*Dpr+%U6H$p4-tuE$eUgU0Lc<5wLC4Z}s9;t!3w* zx;W)@Y$KnmR(W2L{|I>RhrzQ3CAO5C@6rv-8Dl9YqseU+AX9 z&1>i_#}2%8DsI?s0lfcpmUE|W>=53khdd|XSN+!V_VS+Q4Gbp>+shNi{G7G8sajqc z{q=)%FZd<2Gea{L?=u9nyB%xa>|I6PbIAJmD$nYG?O%Q0IK^9*Kd@^t&_3Q<9_zE| zh@1WEfd00P@Ar%DCcl?zRjt0)Tke^1#c=cE*7BUfWuL!yrn_8H(3GSO@ROTeY_K6$ z=^-y@KiyF^va9@9irqc^d~f;o<6|RKn;qomHtXWF-tYxo6EEoe5B8F`z0>aduHAiP zv)}&u*neJg`JRo}`qO86%Mlm9>1#>+Mk3S zoPG%y+DyLl;q>h9G*1Es4AWhlHQ_}-`promkGuAeC%-E{RDIP+c2Ol9Ov~*ld%nM? zG%4zNz?61N8YREq4f2uNB5~!bfK^1XnibPkcK@Wl*?sr!cpi0^2Wf2PZL?P7=Fuzf ze(TpqwpRz$GkoqZ$2aO^0q5`r4`?Gmm7c3zSms; zwz7If!+N>SedJe>sY|X0be0?b+G645KcS!gJbB)VQSIc#Z99$+zvd_(?`GA$VrxZ! zUmu@bLvVk2+;X7!LS z90*h&YS>Dy{A>E#e=QA^e;s+fXZopdd1RHtryp2^$qw7Lp8fINLGr*BiRK>qmrUVb#kk}X4u4UiHtzRoP z&1M>oSie@xmiDVY_3JAIelPV4>oc#E-=%{ybh}?E6TdR-=)C-uayMetR?n0TpWDVd(vr!VjIN@>bAP`w2IUnz^`SKs;ZmzT%{eetv-_p8Xm-<< zl`CEutD{=ZZ_$Gq6=)Aps3Rh+hLzr#!ApNH?&4yJP?L*r$sA*B@ULhCWs9`K+CN((kF#?snMBb}bF1o9iNEfS>Ua#Rs>f35%`@_jj2XlR9ni$AVb1{Llw{4&2QCh$~xcgiL&HX&#-7BGN;Y9^N*E=DMuQl9C@q+cy6{E z`1xZcFC*%FpN)@|;?mq+1D1n5X-!eXsR^N6PO`RiU;$A1Q+^ z3r4(d|44DGzWtkZlSj&lQvul`=@0x@-2PDrI8n-a9=%tWtELQVYx6D&=k0 z-2-mTs8S|qM){phty0#Udvxl1O_lQ9)Kj;wL{%xzuf!!*_pegI|JJZ!`rpIqWR{b;@bOjm)@U0R9ckG@Nc#Cp%Opa-KX)#50#}h6E3z` z0(Sp7F8*^KDu4DneSS>#LnYMu#=|Aa50&cnpNv2I)BOfaLUGzJr_kXDD zJv4I6-ChrsXH6#kF}Bk~mCE4ruQm?(y;51F8(iF4U#a9r_)2z{L4D?gKR;cmIQJO6^y%SBC9s3E%VJ-p zVtM4@ipHN+DjVwGZ5OnmQrT)zG$!MNO2wrxw|-%6r4oL2qyMvaDwS?+=M+U`Rw`|! zA7_4%RH;;~&}mzYt5o(*TRv-jRHd?_)j;@Psf^JyT=kV)soXrYCTE0SrP98BehaC6 zrIPc!`JAtsRVw#}hw7%gRVr7CCprwUuT<)LN$|f?(Kb8sj>VG-C9G(6pQ;BHN>-85 z`>)#-$~S{LPJC2Wp){GZX_wWx3T2eO@rxEGDwKAA^bZ{Lb%k>0VCBS_pI0a!z3mx$ z;L{3agR|a4+EAga+w{IJYGs9TtwY|sg^Mbb`tEa_-RD#&^L}jPv1nR_GCebRp;cOi zB6WEYu_U2FacrACzUi0><@Dv3n-2`DP8gI~eoV^>rA_Fk9%@gpv#mnWsiectt$_`%REAduWmD@fp8fI2pyaK)H~G;6 zgW{~2JKXB7LHVfvvI4Jb2Bp`AZX^0$FetsR9vV93v_Y8?w0Qof#RjEhed`DJzA`BL zA9T#m<@Hb%YS10`qis!gy`x%|7JtXro~?5qZt5P0K=zKb;+-ZT==u2P?T{Xz*rJx1NyITVbeO4l92U=N*O zMnWSf)z}>1*cJAbU|U(o<_PZZgZBck6`&2A*k)6A0ZSPEH#KK9f6l9JG^LmL?_rdo z*0q!Wq5S+UojMMYQGKIs`7cyI$oVU*;JazBr$Z z*v$0!$%$EGbGrMDjf9_`hF?t`i%#+B8HpXW32`juv+$ShbUVt0eLOo5nYgg>|px7Qw=L zfg4?Y<6$FWXjpY^e=*dfe{DAwUtle_Xck{+Z8zpG0^F+eAPxATgREHI{|&T$7`K`+ zV@5(8Q~~i%Hsf`Wkrm%+U=WKhu$J3s7GJ2TTQv6mP~25-Uv zCvq+Y?n9ZkOBXmK4ztbJd5CUZshbzLnY5oDbu+wcMv9=p&|i1zM)J&vrarjw=!s^N z87Zgf$KmfD6q^y92e1}BKqpxV!Ny1S4Pvzim7)Ren30(g-%8CM>DPdpPBOZ&Jc5^h zxk>QFWep@0{K*zCGkY1uLO zrN_-J&4?wqYoUWIg*^}IvpM)TWsgQy#KF&nIPA62T5GH#Xq>c;B`Zjyi+?MeycwpH}xFwm_b|cW)z|AzQM<;vI#LwNLiB!)*WvHijD77o*C4ji03>q&i zLnM@em&I5g`eyK(u3+Cn(}S{_vSc9d>@A7?CToLbvANbv zgX#LIh^7-EKfoR{8S>u5iZr=vN8I4o7Hy*IGxs}SOQJT2gZeXN)36@!yQ$VvBUV?% zG(9^@Vi#v+u+W-o%xJr@zT-d)zVJ>&M?y+yoYd{E730O=R)*Cpg9iZJE zR@i7gT38d6-&&%QRuWsiwU+h|+E#fmwl;?^?sKbJ19kf`@q*0nhutTIm!md=XYfbr z_+xQPtBI|_x|G$Kn#Wy8<7VxVPYf*yInaubTpBMuzwT~?di#XAT?6+mm>%_G%BJBY zcRS*q0DH78ard(!?p<6B_4O*PM2w(h5X2LG| zpDBAZgtolET7jM4(Da1&6lHpD5?UMsoTz&YhW+~AyYgZ|@|0XLWKmc(|GrPe}Y z9wG7&O2fhzpt&YE5Z7pX;tGCUGgMl3K4idoQQwBt_p3+hN7geq=VfdXps*iGUQ0av)(}r=1#!_k zmpTd5vRRSe{?*B`e5fC)FkW<{Doz1V@Bj+v4J9bfH; zLnI%aB>2KQm7gn7U0`D*8*@E;ZAqhOl|>^zCyPcf;KFF8wa5Aj^n$U7**k*W7wq1( z>}ohqc;1{LZz{-}6XdNP@!Z=$@8N3?ZInb@{H_oe=>n-q5!(W~A|D5O^KpRoS=L{m zuS7$;`a!!woAUk!eGdAs8}whdF3x&>U9cy18CF`4=5TG2B8iQ`N-xF_b{&D{Y~Iw0 zG|g#4T$0)n8^2+scoO{9I6S0oq2+z5HbaKMF9E-#j$fX}%D<(dq5TQ2VU2PIKpLTt z#s~CaJQ5Cj5~fj`X&M8-2xmtfzaA}Zh_mz|u`}4{tx9=G*tLYF(anZ*gK*vu&gJ+A zkj_ey2M-tDhy4tuQ=4sS(gFXZj(@4K18JO4k2H>UB#mKwY#ixsXrOm3tzQiFO|L<7 zAx$<0L%)PO)t_OHp2F+QlqEBXjXBVCY-3^J9>$rC5w^A3EN?0Q40C8UAsuwk2jD)J zZcE|#W7q}b^)kYl-B<8_Ov6Gokc;52685$jwgist=D>XjvI`tHgk7s_F_(}E*cUCqqeL8` zOd>z;EQim7uy2C=P?fCBiFGg^w^>{?KAH$@W@Z+|9MUnrwKUy8ns4}GV5clPZo)4Nv3@TR4{yk8LR;uBtsu`{gr$OcHPA2(Y)J#DFL5$B z>g|hdwKf{-2rD(5H}racDTH`c!RPhIaIJ-Sn}dH-X7xk+6TP0(YX#41X}w}P_S!{A z?vVckxW-4rH6H4~w3a>8_N19#j?_#_lj>fB*mz+N@v5wd3hGb=^`+|Kpyhqd5&9Zj zi=l4Vby(aRW!Pw?CJv-YmJ?|Lb<+gKgeEX1G|BKVG}OBla~s_6!nLPKv@O_dNRuvT zYhXtjMAst?pg!UAfn9kF=L6D*bHN?X1!I4+mxw*I?VHkA(oiC+ zU>9kyu_CzU-2BZ`B9*Yy90>L32z8!t9P0fTX(Ao2)erTSkWY5atbHFO*+F0KVx{J` zdeH9B_D~kr6?BY&Ybf+Nx4reWjv9vu$<~b6u7I(QYKB?FoAT}0Mj}mM7x{jZpCw@5 z{4e}m2D@e3|8+d!;Af0UJiH7lD1#G}0m`x$%0T-O^j8n)uO3h~)~8mu8Tk0r0LCXb z7@u58;|n!s4i5u<;eONu+ROvm%p=21&+jkl5jTj(4eH2k1>9e-_-(LqIl#!6^gR~WD1{1o?{q7M!3C6RYwPsKjuT&Ex2>m!kuurCmOtTBwESqrJaB$`!1?I`=ch*`01hk@N2!+Q8aKaidi zM}xiIwzTG|OWUiM)@_~pESlmookVixNF+$4U(bxx>td&t+~HaT=L5SYb#c{r_&RoL z27R;<^wEaUCtSML+l%+F#p1dST?fV}dcM@$=kWVO`0NPhkQ?+@w@7D$$p8HXFh`ne za*ojcltn)mn!~lhjL(foiwNB9mzovxdj}Q0NA|WO-c|1rudMlS?OZ@q(YbJ+ypZVM zmq_m=5~+HdUx!VZ-(NYywLnFjF2Fp9ujBAq<%#-xdFUi3yDyA;bHBW@Rw91uBr+Q7 z&Y6Bzg)yfQv|Xbv4GnI3ms00qC*yUeXsbkig*^-XOSG;18j^;4YoA{p?dd(2H?*bK z8_(Nn$QS>tmS@(;Z>d=$X^~m2&vLF{?+o`2(I1&L^IK`wOj>GIbH(QMsG)wE(fbA` zqRObb?$N#iw>B{Dz%?2^U(&fEelGC)G0}E9m^EJn`(jZRdfyyO>kICQR#>P#+Q2Mg#v`1+FuC5Z`BowuVc!gSrE1I0gWBg(YB7vgu$O`#s{Cw( zjGAn8t%l-Wm=C6Qu@?aEguM`UI>+)gRl=4D5>nvj%(G+DwTWrt`SQVd>JIm!bEzM` zjay(wye>*4bWV+1KDc>bkl38E@TUhix<>G(?J%{$w)%api%@5xPC^}xIs~;Bst>9c zstWbhVjkabsAZ@nsJl^DqRv81LLGtH7u6rt2ek#N3YDNfSj5xSqh3Hgiuxt$cGQ)q zvrx6D<50s;d!hQEwm@}7wL*Qikf(nE^&sjd)J3Q>P$!{AqV`2qqc%mgL#@i?@!vo# zLEVkI0(BPZB-AieHELs2Gt@t^ADl$pi7NU`IzQ0S&)Bf(VETe-CaNDQbYr^d@cUJC z4Ul2jpqtJG6{Y~_rgKJBq0+TT*mQQNaQjL(9o%v-<)vp!7K~S+`%26&T&|e=eB`O9 zNvJ*14$CIYA6*xPO$W0fOkwtgDHlRs!8#1x!Z5r)Y8zBnRD$|oKDH0)S=2+QJ5e{F zu0WlKnuR(6bp&cCYIoFDs47%~`saH*{p+YDs9&NMpsq&EMV*P7f;tX04Amdi8?`a2 z4eGObJl%V!mrzfleu=sbbtS3}H4Swv>LAo!sO?cbQR|`7bu!ZpzLz>Xc6?@HR#v)} zz_->?O3ut=KlldC%s6Yh7Dm!J#^W^j)i;<76bw8YIXg2E*BRep zF52wOBoY5eqss(z8ew#ak55U@%%)@baHE?@t1okz2)T}pO93Cf1W!m#Oi7^M%0bJK z5jzdM5YmlNd`4nyRw54&Ec~Z}1I95ygn`PS?|WmtnE2?y+!8Y~(ldxTJq{Zmn=z5I zC+D=pX*B`ba<{BR_=Q})!q|_+K=Vy&dpsjrA5&vf;P>qP7{{k;;g#h<>^M0yEt~%s z{7lAK>6~-O7-QHp_)Yn-HH7|csY}@S_@u=6$)ZJ?a<_DCmT+libP?%TaF@932@{gj zlCy{xXKhBJmbzso&w%RYZm|goBA({x3yPAFn3a=AS|A;&7XrY0;=DZ)r)0;b zz%^SiR0aJ`25N0FqB~Wyz!Hndz3F%}DnUIl~XhiZEwbIj!S-&L7X^C0m zwb1%xWK@^&UAwR-$WEz$dPZt&R(N7&X6(eoAXv4tj7mvN)DnZ)fW)k*>6uxHsnN-) zi9wn4Hz4T|8Qd>&NWU-|N4>I=`$ zWc|&MWKz01fr8Q!qO{3rtj@?z%jlHMpcKd_A-m}@WN!o;m-bMC%yHU9sXugA^0`DM zEIBSC7P`lNS{~X^LC~wAM-p<KYP3I|aGUj?+`2Lqh4(;Kcm7j>||=X&w&4WODHIEcgM{k;w^(AxW_r#L-F|MyDI? zM5gKVOPiXUk)DV;(*8yZ8Iev0 zx(xdBt~aPJ9(^>_7K9r~eVNqeNLJHP>@qN%9Y-eedfzIs&P`(shATy42K0dDRwJSN zvR|Mj23`kgnQ*&6Ys`$M1%nP{IDz0QF`uw@!G`;ugs>|%ZwD_P6Z(gY{lD#hqy}t$ zm-&_I0RR zP)+^OJKBcmuoLb3>iDB+ncn$6_=%UZunSMO8rgts8b3Y0EBuGNIiJG6|1}=3rU&;I zid^i^Ii?OLA?Nkv_8ep_axSt4c{Q>Qxd2&@=@lXu`|6=D6e4|3r!&VB2Ik3h~J%I*3q*#09q$Dmz<;mgoo zID*@g&|Zr9$wAgeaeFSZ8hN$wAIa?n$nX;cEWi7Z3nMs-`tZW?l%PEy!*C+M~*=DMlQtsiFOcr9nY(XaGu{P^shmFfviOq^{qn|@d(vm zduvAV_>z!|k%d2@WoY-p{#P#g3vwu?ug3lsQ-`b3e8F0&zSM^;6uH*RQCbe;VIJ{b;&# z|Drzh$aTjnQU20y+`lNlU{U@Ew2SiTkwy9btK*#*4>S{bdfwQ-#dsjjcVQRjH+<0= zJKti^e?$UjaehZ6an3<|DcW<9^O42*snc-(#iIYkaTe!m;9H!}pj|zVvp9eAFgO{^q*k8ObK5;(77pt-Iit|ROUR)Q)a=RY;pBLu099fMl z!U_H1Ew2j`F1>!;Wz$XHk2}EksIBJjY>4Z1Col9#7h_-(YXh9%`EWR(bCKfOUe_-C znX2SvO8>rNGl-Yw``^8pwW=xK|8CqGd;T7Y$fr;muj%o>N+)#fo8Lzf;r`Qhkq@Ey z`ESmz-jTa&oVe=E-|SDs^G~Y(n1_qNdI|X_kN-Az{$|vh{_-Te{&D-aW#K;>QEXg% zLgIvpNy(EYr=+H(Yp1}+`0T0Ea;DGV*$N5{>D#aWfY5=128V?Y85%JxGAerbh>6jLP|0A2PZua8lzx|!_iaQ0O?*A|P4?_Q6g8y@y?r!oVzicYsH0Gb< zpS6*N7-mVxu&d*)dXe#0CtdCGcmEg5Zs?!F?9?otvsd^mfbi(Qk&m+vzWDd(Y~1=_ zyatT-zpEV~gLMDDeUtvhW>#l2$4!)*win$5|99>wM411l|G64aso=ZIVV{?5R_nb& z`o7^6*w=wO)jwb_gku9}GCY(}O#OqxBWvt95VQe2ypRCyYS4+Wvp7Lh;o*iX>L2tJ z?9@*ws4qNZK;IEngT}#L2FE(k`tVQ$4eJHE(-T&nC&4p7pyItls^Wb^s;^=11AeGB zhj$wz;F#)W*b``2Q1QMa)g0I#K>So!!%n~t%y$x)17&%jn$VOVYe5g=aWQCmGpG~r zuLZq?$ENQeI(tE0VBZOTs7`~u7>;$IjbIRG z*TON(rIKG^r(sRsWix%xjlS=;q%|Q$;GgPd*h}G<>UG%la13*OI&kCPN-T#e0!dN5bbgS8%7A1G_gI>p)M# zPTRB;v=a7Gh==$c@xspkM|jvQy~ZN80Kq9-)`I; z<~GPFJcfAUux)(oI zf#$)!5As9Rw-3BS3CC*CD%d0780NFcOd0w;%`>Pf5c&))J7{nae7PsM(>K=3@z?;m zJp|5c8WvQ%14uP$0E`Rp4j$EQu!q4h)%FAbzxK{P$gS$U!)IL+6nC2lYKR&;MfK3E zC^nYMwpE?A5s6Jj*b-r{u>b>Fylcy3Z7W{ekr}6m@L>fEBNIbpL)J;&$Cdq?gn$dcn+(wNaB!ZnCj3P@;0SDJb`B76P~j?&sEi8B;`DVRd-lE3{#e` z!4HjDYZ;CrsXsg4Q1TGR*D3Wf=Mp}E+IXHrt8a;?KF@&FoEuHg^H=q8%kwN%-EH|O ztXjSXU$cA<-nQPlM))L>HZ@ocForS?aO@_toadwJIm`2GRM~DeW7rN~Luxx~5PEJg z%l$Biq|F+94b8+qd~gHtLH#%^B3TPHc)_h^9iCmO%Wq?Ep$^Zj)LlXT_7xw6ua7f- zc>ZKZeS&|BpTP4hO+AWc`VU{RJkQY7Tb7p>{VzsRhi6yn;!Wl}@?1;Zw3+kL4$th= zlW3+L_#MmhEKFS(GRt|+rA95!GduNw<$0c_-b8YZ@;_B@qnUHT*U$vzJ^1{cX8G*A zN6Cl0aEj|Ck5IS_jo^hIG>Z4aS2!is3tO+1>1M_+fY-lKR5KQTBYwk3i==+zau- zwV!8>cg#^^@BoteJqVjf#!z_ZUhbFFIRdLl+N{Cr?qeULd=x&5!uVrw$^Go1csD$S zVtD?MTP;9x>_WKC@&UNt^1`I$Gw_7vg|AxvG+echzrUeP;Z7uN3S*WRej7>uPr}<{ zrVqkCI>@oYg%DtG5BHlT_kaG3U2%|<3s-B1pJZZ&%iAjpK~amg6AjA^X`NfrI=IN zall0x#u|SqOdz>-p*L%eiSVhM>5sw>7kEBTn{k*eF<*EcP8jB~!pkb=p0gO9Up3Eb zgHIx9UxT}9tWA!Mzy(jyK7E*iUqv!j$KWX`XTF5zEWh|^_I#AsMf}6NzGC{l@R{cr zAIj_ShtIRl@jdv#7tFD;!+l7`DhA)Oyn2ya<4b%-r92F;KW=(Iy!ji9A?<8{Z=n&q z`X<+o#OrY539b=80k8iSdl%jh(@6H544i72UYJF4E*-vV`P1;BlkD4+?}tD92gVBD zgVS&D84kY_N`9<-UYeaJEBUjk&)TPs@LHtfyYSopWuEsWeElcp*q(uRoHhLf{O~zG z)6(aK@UM}KzXososp$joRV3FEdrO{)49v`9Xn_3R-y#Q#I9v#yMh@CJ3K!2Em}y5i zcHV&6K!1eaL{k3*T>HL(nKsAZP2|9|Id%ga9V8D4FWik}tRgUF`3Ag{d~In*IF6*x z5%`l2nC0i-u;i)kX1%~4BL{x2eLywPAYM4{0`po1;bzMpgzJBozR`yOd5Rp!QOJ6#f{6@qKt9d8`t{^Whbicf*k7!?1*oP~U(rI>>wD_nnvF zo9H0rBfmGGZb#D21l(zP;ZsQZb_|}gyl_4Fu~L5uMlCOlTV8nF@=e&bys(cN(to&q zf!TflR*)lbD{A1w@EH`r*WrhT7*D(d{tH@# z??Ri)Jg@MlXq0kw8S{>09z(Ezq(26X{*f7|YlGkYW5%8SoPw7Ovwo=KglqnU{^Q4Bz{5CGNBCDr z#xn}P{OA0PIvPBSc>H6G8@>rol4mQ&3O~4l{4Km4`jL!@ z@NP77E*M)mpiWUo_y+3Z&%iBLndhB?Q8ZH@Hm&m0aDmsX{u zB)=Ey(DNyCe-JLXVL+Xr&O-RTHI(B!FtL_(hZpvdT#Io2nCXRoe8H(c{sCMxWv=~Ecm_!w zVQ?$`Bj;6k9LY5{VR4)34fxMd?l;uwzytTN9`Og^NhEy}Uio>m&0)CmUiNqDc%gx0 zuM^I_&%7?-;Qfpbb%ci=AWrZ{;MM!gKCglYkuK+gBjozl@%!O5UtnJOy~zikM{+Im zAL4xg`zfc+?A%?+>D?aZe0%52@L{|gABP(cnAbP~=R8UdB{6J=y~pS$%*hjP5Hg8D8vr@(yR=fX~r*rkq8mCQL3j#^%L#PUId z{PCxm3+fNTESiZuc;#2j{tv^)(O&AzJvyMSLK@x+D@bCX1}}Ps`J&tbzk{Tmlkn`{ z45%9A&c9_Hp%z|PLvmjeUQ?$X%6;%zbQ*sQ-ukRLK0)}AubTZCfsde>XMNB>GXBCl zpELKvD2yW;`Lr$gpGe+K)Q9JNjq#-12LBYvu^zY%$+ZOFNpvZ7gl|~oXW)4avwjek z(I|DCFAk`GMPa;piMc>MKAXwAisT(bCpoWB-ajN>DDMywFO+xw1gI~R_w5|T3*}t_ ztJoWb@=g+|FO>I2h!^g&yinemAmu`NkAZlhyr)3CP~IsZUMTMk5HFPX0Eib#KEHUO z- z$cy|ah}ov|AFple``)PZ|nQ>>l8Gk0231_02ct*?AGL1|t)5-K`(@vZ2 ztT*e=2D9O8G#k%q*;=-dZDl*zURLGoIcLtD^XB}yU@n}C=HfXmSIaeWtz0M9%c;CQ z@65aDjh~)`=|!9#)bfpdE8ofY@=CYsPTj40b-y0e!+KPY>zZEE8+uFc=sjH(>;-4R zUGNtCgG&tu~qC8dqq{U z^9+l0CS?chWGC58VptJ2iT9K}tp-_%s_}ah%gH= z<{`mM=*&f(*=RB!ZDyp)ob;I$8}s5|W?amThuQHlKLKVa#2iJKr5N*+$SFJbX~rR_ zg|x61(HdG)YiVt*qjj~O*4I?hmb51wNoUfPbSFJYZ_=0aCj&_ju@xY$BE(dJc&g`{ z`F6gW@8@m0LwD&O-KPiikRH)vdP3Lry57{=dROo3wt}PJDtHRMLZA>TL<+G&qM#S* zg=V2$=ob0~ThUQ;6+J~?F;EN@BgI%TQPhj|VzbyTc8mR@t>h@VN}iIh6exvCky5Ob zDCwnosaa~5x}|=}W;hI&;W2zhzz7);BW5HF-KZN)qiuAJzF{jn%C54f>?;S#p>m`g zD<{f&xn6FT+vRS#U$#{o6<5Vm@l^tqP$g1{RT34wQm-^C?Mk=Ouh^=Ns;la$`l^9y zs2Zups)?#ztyi1XcC}mW^ZZjec~${znnQDG9?ho(7~O~#(-NAl)fwS7qubYPjH`<= z^)a5IWF#3&CX#xxo@^%D#ArWhOF4*44^b&G86h4MDcy?3E|F*>3SC5?kLU{#d9idN zt*7hhX1Yxb_R}_^&qd_%jj1et&V6+$C?xUkaAOrD!Q$(n_^bqtq&OO1+XY?1s~D8(zb21dXr}HR6V5)QpDF zGCD@jP-T1BS$3DbWq&zX4ws|lcv&mg%8hcX+$s0Us$#D=EAEQ7;;#fN;Yzd;uV|H8 zrBP{BI+b2URqa)0)m`;g{ncPKT#Z)aRjpd9HmWMf=X9nuF|$Kp;{1*vd!A;~?8K&< z*z^*ee&X}(^&cljwV7Su?e#Bf|Ls*DBu>LbYLr-w6RjHYS|ef`#B7VG?a1DmvazQ+ z-m|L)-?Ojk#87i4ihi}7HA;lXiE)i6uMy`BBE3bdcYeu^8-CYr+pu=p_wKSD?sC5W z)js>H?6F!-=YCk{j@aa$*yf(t=dNhuzUbi2=;Ge!`5*7qHFnnq`)i9Gw!?3y8VOr5>5$6l$}C%q-5@yt-Bo!zI#{xf@z@4st@r`m76 J{tu17e*^hfY>WT^ literal 0 HcmV?d00001 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