Использование мессенджера MAX для обмена данными в QUIK

Страницы: 1
RSS
Использование мессенджера MAX для обмена данными в QUIK, Использование мессенджер для обмена для отправки сообщений и управление торговым приводом в QUIK
 
Поскольку , эта тема вроде нигде не поднималась , опишу свой опыт , может быть будет кому то полезно.
Для информирования и  контроля за работой торгового привода у меня был поднят небольшой   саит на локальном компе с QUIK ,и меня все  устраивало,
Сейчас эта схема практически утратила смысл из за почти постоянного отключения мобильного интернета. Я решил  посмотреть  , что можно использовать взамен.  Нормально работали только саиты из белого списка и MAX.  Поскольку других вариантов  не было , выбор пал на MAX ,
И так , что же мог предложить MAX для решения этой задачи ?    В MAX можно создавать чат боты , но делать это могут только OOO ,ИП и самозанятые.
Я давно имею статус самозанятого , а для тех кто его не имеет , получить его можно онлаин за 5 минут и этот статус ни к чему не обязывает.
Для самозанятых MAx ставит ограничение в 2 чат бота. Мне хватило 1 бота, и 1  канала.
Я зашел в лк MAX (https://business.max.ru/self/services ) как самозанятый и зарегил бота , создал там же канал.  Далее чат бота я сделал администратором канала.
В чат чат- бота я направил информационные сообщения от робота а в  канале закрепил набор кнопок для  отправки управляющих команд для торгового привода.  Правда пришлось помучиться  с вычислением идентификаторов каналов chat_id. Проще всего это сделать запустив web версию  и взять эти id из URL   (у чат бота это положительное число в конце URL ,  а у канала отрицательное).

Привожу пример отправки сообщения в чат бот Max.
Для отправки HTTP сообщения я использовал c библиотеку  luacom.dll +WinHttp.WinHttpRequest.5.1, но есть и много других способов (напимер curl)
Код
-----------------------------------  
function send_max(msg,chat_id,access_token,nFormat)
   -----------------------------------
   ---грузим luacom
   luacom = require("luacom")
   
   --для формата markdown
   -- # Формат markdown \n **жирный**, _курсив_, ~~зачёркнутый~~, ++подчёркнутый++, `моноширинный`  ,   ^^Выделенный^^ , [Документация](https://dev.max.ru/) 
   local url,status,responseText,cUser,body,cFormat,http
   --chat_id =117.......474
    --access_token = "f9LHodD0cOIq...................0BhMw0gm_6f1-DBY" 
   if not nFormat then 
      nFormat=0
   end 
   ---выбор формата разметки 
   if nFormat==0 then 
      --без формата 
      cFormat=''
      elseif nFormat==1 then 
      --HTML
      cFormat=   ',"format": "html"'
      elseif nFormat==2 then 
      --markdown
      cFormat=   ',"format": "markdown"'
   end 
   
   -- URL   
   url = "https://platform-api2.max.ru/messages?" .. "chat_id=" .. chat_id
   
   --Тело запроса (JSON)
   body ='{"text":"' .. msg .. '"'..cFormat..'}'
   -- Создаём COM-объект
   http = luacom.CreateObject("WinHttp.WinHttpRequest.5.1")
   if  http then
      -- Открываем соединение (синхронно)
      http:Open("POST", url, false)
      -- Заголовки
      http:SetRequestHeader("Authorization", access_token)
      http:SetRequestHeader("Content-Type", "application/json")
      http:SetRequestHeader("Accept","application/json")
      --Отправляем
      http:Send(body)
      --Получаем ответ
      status = http.Status
      responseText = http.ResponseText
      if http.Status==200 then 
        ---ok
      else    
          message("Status: " ..  tostring(status), 3)
          message("Response: " ..  tostring(responseText), 3)
      end    
      -- Освобождаем объект
      http = nil
   else    
      message("Не удалось создать WinHttp.WinHttpRequest.5.1", 3)
   end
end 
 
Спасибо,Евгений, для тех кто не в теме и для меня лично, поясните, а зачем все эти премудрости? Что это дает? и А каком приводе идет речь, типа Бондаря для скальпинга?
 
Цитата
VPM написал:
Спасибо,Евгений, для тех кто не в теме и для меня лично, поясните, а зачем все эти премудрости? Что это дает? и А каком приводе идет речь, типа Бондаря для скальпинга?
Вы же не можете сидеть безвылазно перед компьютером с quik ом .Нужно отлучаться  по делам или на  работу или на отдых. И хочется контролировать работу робота , иногда  поменять стратегию  , или вообще остановить в случае форс мажора или поднастроить немного. Посмотреть  отчет по сделкам , по состоянию депо и.т.п.  Для таких же целей банки делают свои приложения на смартфоны  . А привод в данной теме речи не играет. У меня своя самомиска для торговли на спредах  на облигациях.
Цитата
 
То есть если я правильно понимаю, контроль за роботом (Вашей самопиской) в окне МАХ? А во сколько можно оценить потерю во времени прохождения команды? Ну хотя бы примерно ведь наверно задавались данным вопросом. Если можно продемонстрируйте картинкой.
 
Цитата
VPM написал:
То есть если я правильно понимаю, контроль за роботом (Вашей самопиской) в окне МАХ? А во сколько можно оценить потерю во времени прохождения команды? Ну хотя бы примерно ведь наверно задавались данным вопросом. Если можно продемонстрируйте картинкой.
У  ботов от макс  есть 2 варианта обмена   основной - через Webhook   и Long Polling .  Long Polling - это что то типа временной заглушки для тестирования.
Я использую  Long Polling . В цикле MAIN quik а раз в 5 секунд  выполняю опрос бота , разбираю ответ и выполняю принятые команды и отсылаю результат в чат  , если требуется. Получается на ответ от 6 до 7 секунд.Через Webhook можно сделать быстрее  , но там надо намного больше заморачиваться. Читайте   API max  , если есть желание разобраться подробнее.    
 
Еще раз благодарю Вас за пример и просвещение. Меня собственно заинтересовало, возможность получить дополнительный вывод.  Для не большого скрипта  - стохастический генератор Monte Carlo generator (WinRate + ProfitFactor  Lua 5.4). Используемый для проверки гипотезы.  Но мне нужен переход от генератора результатов к симулятору позиции.
Отличие принципиальное:
Mодель генератора:  Trade -> Result
Новая:  State(t) -> Price \ Path -> Position  -> Exit  -> Capital. То есть сделка становится процессом, а не точкой. Ну и соответственно вывод со сменой входящих параметров.
Надеюсь не запутал? Подходит под подобную задачу?
 
Цитата
Евгений написал:
Поскольку , эта тема вроде нигде не поднималась , опишу свой опыт , может быть будет кому то полезно.
Для информирования и  контроля за работой торгового привода у меня был поднят небольшой   саит на локальном компе с QUIK ,и меня все  устраивало,
Сейчас эта схема практически утратила смысл из за почти постоянного отключения мобильного интернета. Я решил  посмотреть  , что можно использовать взамен.  Нормально работали только саиты из белого списка и MAX.  Поскольку других вариантов  не было , выбор пал на MAX ,
Код
Странно, если мобильный интернет не работает то и МАХ не должен работать.
скорее всего не работал DNS cервер, поэтому не могли найти IP адрес
----------------------------
из моего опыта .
Кратко.
можно не делать сайт ,
а создать сервер на локальном компьютере там же где квик  
Потом сообщить на смартфон IP адрес своего локального компа ,
у которого есть выход в интернет.
Будете работать напрямую со своим сервером.
------------------------------
Еще варианты прокси или VPN  можно бесплатные
 
Цитата
nikolz написал:
Цитата
Евгений написал:
Поскольку , эта тема вроде нигде не поднималась , опишу свой опыт , может быть будет кому то полезно.
Для информирования и  контроля за работой торгового привода у меня был поднят небольшой   саит на локальном компе с QUIK ,и меня все  устраивало,
Сейчас эта схема практически утратила смысл из за почти постоянного отключения мобильного интернета. Я решил  посмотреть  , что можно использовать взамен.  Нормально работали только саиты из белого списка и MAX.  Поскольку других вариантов  не было , выбор пал на MAX ,
Код
    
 
Странно, если мобильный интернет не работает то и МАХ не должен работать.
скорее всего не работал DNS cервер, поэтому не могли найти IP адрес
----------------------------
из моего опыта .
Кратко.
можно не делать сайт ,
а создать сервер на локальном компьютере там же где квик  
Потом сообщить на смартфон IP адрес своего локального компа ,
у которого есть выход в интернет.
Будете работать напрямую со своим сервером.
------------------------------
Еще варианты прокси или VPN  можно бесплатные
Мобильный интернет то работает , только  ничего  кроме max ,  приложений некоторых банков и мир pay не открывается.  Когда объявлена беспилотная опасность , то все кроме белого списка блокируется наглухо. А у нас в краснодарском крае эта опасность объявлена почти  постоянно .Может на счет сайта я  не совсем точно выразился . У меня  работает локально на компе с квиком web сервис +корпоративный open vpn  , так как выделено ip у меня нет  .  
 
Еще один вариант.
Особо не отлаживал.
Выкладываю как есть:
Код
-- max_bridge.lua
-- Двусторонний мост QUIK <-> MAX Messenger
-- Lua 5.3/5.4: native bitwise, //, math.tointeger, <const>
-- Очереди: T={{},0,0} — массив + jw/jr, сброс в push

local MAX = {}

--========== КОНФИГУРАЦИЯ ==========--
MAX.TOKEN       = "ВАШ_ТОКЕН"
MAX.API_URL     = "https://platform-api2.max.ru"
MAX.CHAT_ID     = nil
MAX.LAST_MARKER = 0

MAX.LAST_SEND   = 0.0
MAX.LAST_POLL   = 0.0
MAX.ERR_COUNT   = 0
MAX.HTTP        = nil

local luacom_ok, luacom = pcall(require, "luacom")

local MSG_GAP    <const> = 2.0
local POLL_GAP   <const> = 2.0
local ONIDLE_RET <const> = 200

local F_ACTIVE <const> = 1
local F_SELL   <const> = 4

--========== ОЧЕРЕДИ ==========--
-- T = { buf, jr, jw }
-- T[1] — массив данных
-- T[2] — указатель чтения (jr)
-- T[3] — указатель записи (jw)
-- При jr == jw (очередь пуста) — сброс в 0, следующая запись в buf[1]

local function setQ(T, X)
    local jw = T[3]
    if T[2] == jw then
        jw = 0
        T[2] = 0
    end
    jw = jw + 1
    T[1][jw] = X
    T[3] = jw
end

local function getQ(T)
    local jr = T[2]
    if T[3] > jr then
        jr = jr + 1
        T[2] = jr
        return T[1][jr]
    end
end

local function qsize(T)
    return T[3] - T[2]
end

MAX.INCOMING = {{}, 0, 0}
MAX.OUTGOING = {{}, 0, 0}

--========== ТАБЛИЦЫ КОДИРОВАНИЯ/ДЕКОДИРОВАНИЯ ==========--

local escape_map <const> = {
    [92]  = '\\\\',  -- \  > \\
    [34]  = '\\"',   -- "  > \"
    [10]  = '\\n',   -- \n > \n
    [13]  = '\\r',   -- \r > \r
    [9]   = '\\t',   -- \t > \t
}

local unescape_map <const> = {
    [110] = '\n',    -- n  > \n
    [114] = '\r',    -- r  > \r
    [116] = '\t',    -- t  > \t
    [34]  = '"',     -- "  > "
    [92]  = '\\',    -- \  > \
    [47]  = '/',     -- /  > /
}

--========== КОДИРОВАНИЕ (escape) ==========--

local function json_escape(s)
    local res, j = {}, 0
    for i = 1, #s do
        local b = s:byte(i)
        local rep = escape_map[b]
        if rep then
            j = j + 1
            res[j] = rep
        else
            j = j + 1
            res[j] = s:sub(i, i)
        end
    end
    return table.concat(res)
end

--========== ДЕКОДИРОВАНИЕ (JSON-парсер) ==========--

local J = { s = "", p = 1 }

local j_skip, j_val
j_skip = function()
    while J.p <= #J.s do
        local c = J.s:byte(J.p)
        if c == 32 or c == 9 or c == 10 or c == 13 then J.p = J.p + 1
        else break end
    end
end

j_val = function()
    j_skip()
    local c = J.s:byte(J.p)
    if c == 34 then
        J.p = J.p + 1
        local buf, i = {}, 0
        while J.p <= #J.s do
            local ch = J.s:byte(J.p)
            if ch == 92 then
                local n = J.s:byte(J.p + 1)
                local rep = unescape_map[n]
                if rep then
                    buf[i + 1] = rep
                else
                    buf[i + 1] = J.s:sub(J.p + 1, J.p + 1)
                end
                i = i + 1
                J.p = J.p + 2
            elseif ch == 34 then
                J.p = J.p + 1
                return table.concat(buf)
            else
                buf[i + 1] = J.s:sub(J.p, J.p)
                i = i + 1
                J.p = J.p + 1
            end
        end
        return table.concat(buf)
    elseif c == 123 then
        J.p = J.p + 1; j_skip()
        local obj = {}
        if J.s:byte(J.p) == 125 then J.p = J.p + 1 return obj end
        while true do
            j_skip()
            local key = j_val()
            j_skip()
            if J.s:byte(J.p) == 58 then J.p = J.p + 1 end
            obj[key] = j_val()
            j_skip()
            local sep = J.s:byte(J.p)
            if sep == 44 then J.p = J.p + 1
            elseif sep == 125 then J.p = J.p + 1 break
            else break end
        end
        return obj
    elseif c == 91 then
        J.p = J.p + 1; j_skip()
        local arr = {}
        if J.s:byte(J.p) == 93 then J.p = J.p + 1 return arr end
        while true do
            arr[#arr + 1] = j_val()
            j_skip()
            local sep = J.s:byte(J.p)
            if sep == 44 then J.p = J.p + 1
            elseif sep == 93 then J.p = J.p + 1 break
            else break end
        end
        return arr
    else
        local start = J.p
        while J.p <= #J.s do
            local b = J.s:byte(J.p)
            if b == 32 or b == 9 or b == 10 or b == 13
               or b == 44 or b == 125 or b == 93 then break end
            J.p = J.p + 1
        end
        local v = J.s:sub(start, J.p - 1)
        if v == "true" then return true
        elseif v == "false" then return false
        elseif v == "null" then return nil
        else return tonumber(v) or v end
    end
end

local function json_parse(str)
    J.s = str; J.p = 1
    return j_val()
end

--========== HTTP ==========--

local function http(method, url, body)
    if not MAX.HTTP then return nil, 0 end
    MAX.HTTP:Open(method, url, false)
    MAX.HTTP:SetRequestHeader("Authorization", MAX.TOKEN)
    MAX.HTTP:SetRequestHeader("Accept", "application/json")
    if body then
        MAX.HTTP:SetRequestHeader("Content-Type", "application/json")
        MAX.HTTP:Send(body)
    else
        MAX.HTTP:Send()
    end
    return MAX.HTTP.ResponseText, MAX.HTTP.Status
end

local function raw_send(msg, chat_id, nFormat)
    chat_id = chat_id or MAX.CHAT_ID
    if not chat_id then return false end
    chat_id = math.tointeger(chat_id) or chat_id

    local fmt = ""
    if nFormat == 1 then fmt = ',"format":"html"'
    elseif nFormat == 2 then fmt = ',"format":"markdown"' end
    local body = '{"text":"' .. json_escape(msg) .. '"' .. fmt .. '}'
    local url  = MAX.API_URL .. "/messages?chat_id=" .. tostring(chat_id)
    local resp, status = http("POST", url, body)
    if status == 200 then
        MAX.ERR_COUNT = 0
        return true
    end
    MAX.ERR_COUNT = MAX.ERR_COUNT + 1
    if MAX.ERR_COUNT <= 3 then
        message("MAX: " .. tostring(status) .. " " .. tostring(resp):sub(1, 150), 3)
    end
    return false
end

--========== ПУБЛИЧНЫЙ API ==========--

function SendToMAX(msg, chat_id, nFormat)
    setQ(MAX.OUTGOING, { m = msg, c = chat_id, f = nFormat or 0 })
end

--========== POLLING ==========--

local function poll()
    local url = MAX.API_URL .. "/updates?marker=" .. tostring(MAX.LAST_MARKER)
               .. "&timeout=0"
    local resp, status = http("GET", url, nil)
    if status ~= 200 or not resp then
        MAX.ERR_COUNT = MAX.ERR_COUNT + 1
        return
    end
    MAX.ERR_COUNT = 0
    local data = json_parse(resp)
    if not data then return end

    if data.marker then MAX.LAST_MARKER = data.marker end
    if not data.updates then return end

    for _, upd in ipairs(data.updates) do
        local utype = upd.update_type

        if utype == "bot_started" then
            if upd.chat and upd.chat.chat_id then
                MAX.CHAT_ID = math.tointeger(upd.chat.chat_id) or upd.chat.chat_id
            end
            message("MAX: chat_id = " .. tostring(MAX.CHAT_ID))
            SendToMAX("? Связь установлена. /help — список команд", nil, 2)

        elseif utype == "message_created" then
            local msg = upd.message
            if not msg then goto next_upd end
            if msg.recipient and msg.recipient.chat_id then
                MAX.CHAT_ID = math.tointeger(msg.recipient.chat_id) or msg.recipient.chat_id
            end
            local text = msg.body and msg.body.text or ""
            setQ(MAX.INCOMING, text)
            message("MAX < " .. text)
        end
        ::next_upd::
    end
end

--========== ДИСПЕТЧЕР ==========--

local function dispatch()
    local now = os.clock()
    if now - MAX.LAST_SEND < MSG_GAP then return end

    -- Приоритет: исходящие
    if qsize(MAX.OUTGOING) > 0 then
        local item = getQ(MAX.OUTGOING)
        if raw_send(item.m, item.c, item.f) then
            MAX.LAST_SEND = os.clock()
        end
        return
    end

    -- Затем: подтверждение входящего
    if qsize(MAX.INCOMING) > 0 then
        local text = getQ(MAX.INCOMING)
        local reply = MAX.build_reply(text)
        if raw_send(reply, nil, 2) then
            MAX.LAST_SEND = os.clock()
        end
        return
    end
end

--========== ОБРАБОТКА КОМАНД ==========--

function MAX.build_reply(text)
    if not text then return "/help — список команд" end
    local cmd = text:lower():gsub("^%s+", ""):gsub("%s+$", "")

    if cmd == "/help" or cmd == "помощь" then
        return "Команды:\n/status — состояние\n/positions — позиции\n/orders — заявки\n/trades — последние сделки\n/help — список"

    elseif cmd == "/status" or cmd == "статус" then
        return "QUIK работает. " .. os.date("%Y-%m-%d %H:%M:%S")

    elseif cmd == "/positions" or cmd == "позиции" then
        local lines, n = {}, 0
        for i = 0, getNumberOf("FUTURES_CLIENT_HOLDING") - 1 do
            local p = getItem("FUTURES_CLIENT_HOLDING", i)
            if p and p.openqty ~= 0 then
                n = n + 1
                local qty = math.abs(math.tointeger(p.openqty) or tonumber(p.openqty) or 0)
                local vm  = tonumber(p.varmargin) or 0
                lines[n] = string.format("%s **%s** %d шт, вм %.2f",
                    p.openqty > 0 and "Лонг" or "Шорт",
                    tostring(p.sec_code), qty, vm)
            end
        end
        return n == 0 and "Нет позиций." or table.concat(lines, "\n")

    elseif cmd == "/orders" or cmd == "заявки" then
        local lines, n = {}, 0
        for i = 0, getNumberOf("ORDERS") - 1 do
            local o = getItem("ORDERS", i)
            if o and (o.flags & F_ACTIVE) ~= 0 then
                n = n + 1
                local qty = math.tointeger(o.qty) or tonumber(o.qty) or 0
                local price = tonumber(o.price) or 0
                lines[n] = string.format("%s **%s** %d @ %.2f",
                    (o.flags & F_SELL) ~= 0 and "Прод" or "Покуп",
                    tostring(o.sec_code), qty, price)
            end
        end
        return n == 0 and "Нет заявок." or table.concat(lines, "\n")

    elseif cmd == "/trades" or cmd == "сделки" then
        local lines = {}
        local total = getNumberOf("TRADES")
        local start = total > 5 and total - 5 or 0
        for i = start, total - 1 do
            local t = getItem("TRADES", i)
            if t then
                local qty = math.tointeger(t.qty) or tonumber(t.qty) or 0
                local price = tonumber(t.price) or 0
                lines[#lines + 1] = string.format("%s **%s** %d @ %.2f",
                    (t.flags & F_SELL) ~= 0 and "Прод" or "Покуп",
                    tostring(t.sec_code), qty, price)
            end
        end
        return #lines == 0 and "Сделок нет."
             or "Последние:\n" .. table.concat(lines, "\n")

    else
        return "Получено: " .. text .. "\n/help — список команд"
    end
end

--========== CALLBACKS QUIK ==========--

function OnInit()
    if not luacom_ok then
        message("MAX Bridge: luacom не найден!", 3)
        return
    end
    MAX.HTTP = luacom.CreateObject("WinHttp.WinHttpRequest.5.1")
    if MAX.HTTP then
        MAX.HTTP:SetTimeouts(3000, 3000, 5000, 5000)
        message("MAX Bridge: запущен. Lua " .. _VERSION .. ". Ожидание /start...")
    else
        message("MAX Bridge: WinHttpRequest не создан!", 3)
    end
end

function OnIdle()
    if not MAX.HTTP then return 1000 end

    local now = os.clock()

    local interval = POLL_GAP
    if MAX.ERR_COUNT > 3 then
        interval = POLL_GAP * (MAX.ERR_COUNT < 10 and MAX.ERR_COUNT or 10)
    end
    if now - MAX.LAST_POLL >= interval then
        MAX.LAST_POLL = now
        poll()
    end

    dispatch()

    return ONIDLE_RET
end

function OnStop()
    local out_n = qsize(MAX.OUTGOING)
    local inc_n = qsize(MAX.INCOMING)
    if MAX.CHAT_ID then
        if out_n + inc_n > 0 then
            raw_send(string.format(
                "?? MAX Bridge остановлен. В очередях: %d исх, %d вх.",
                out_n, inc_n), nil, 0)
        else
            raw_send("?? MAX Bridge остановлен.", nil, 0)
        end
    end
    MAX.HTTP = nil
    return 1000
end

function OnTrade(trade)
    local is_sell = (trade.flags & F_SELL) ~= 0
    local qty   = math.tointeger(trade.qty) or tonumber(trade.qty) or 0
    local price = tonumber(trade.price) or 0
    SendToMAX(string.format("?? %s **%s** %d @ %.2f",
        is_sell and "Прод" or "Покуп",
        tostring(trade.sec_code), qty, price), nil, 2)
end
--[[
Инструкция: мост QUIK - MAX
Шаг 1. Создание бота в MAX
Откройте MAX на смартфоне, найдите бота Masterbot (или через бизнес-кабинет , если есть юрлицо/ИП).
Создайте нового бота, получите токен — длинная строка вида f9LHodD0cOIq...0BhMw0gm_6f1-DBY.
Напишите боту команду /start — это нужно, чтобы бот «увидел» вас и скрипт позже сможет подхватить ваш chat_id.

Шаг 2. Установка скрипта в QUIK
Откройте скрипт в любом текстовом редакторе (Блокнот, Notepad++).
Найдите строку MAX.TOKEN = "ВАШ_ТОКЕН" и вставьте свой токен.
Сохраните файл, например, max_bridge.lua.
В QUIK: меню Сервис > Lua-скрипты (или Ctrl+I).
Нажмите Добавить, выберите max_bridge.lua.
Нажмите Запустить.
В окне сообщений QUIK должно появиться: MAX Bridge: запущен. Lua 5.x. Ожидание /start...

Шаг 3. Подключение смартфона
Откройте MAX на телефоне, найдите своего бота.
Отправьте ему команду /start.
Скрипт перехватит событие bot_started, запомнит ваш chat_id и пришлёт ответ: ? Связь установлена. /help — список команд.
Всё — двусторонняя связь работает.

Шаг 4. Команды с телефона
Отправляйте боту в MAX:

Команда   Что делает
/help   Список всех команд
/status   Состояние QUIK, дата/время
/positions   Открытые фьючерсные позиции
/orders   Активные заявки
/trades   Последние 5 сделок
Шаг 5. Отправка сообщений из QUIK
Скрипт регистрирует глобальную функцию SendToMAX().
Любой другой Lua-скрипт в QUIK может вызывать её для отправки уведомлений на телефон:

lua
-- Простой текст (без разметки)
SendToMAX("Сигнал: RTS пробил 100000")

-- Текст с разметкой Markdown (формат 2)
SendToMAX("**Внимание!** Цена `GAZP` упала ниже 200", nil, 2)

-- Текст с разметкой HTML (формат 1)
SendToMAX("<b>Внимание!</b> Цена GAZP упала ниже 200", nil, 1)

-- Отправка в конкретный чат (если chat_id известен)
SendToMAX("Личное сообщение", 117000000474, 0)
Форматы:

0 — без разметки (по умолчанию)
1 — HTML (<b>жирный</b>, <i>курсив</i>)
2 — Markdown (**жирный**, _курсив_, `моноширинный`)
Все сообщения попадают в очередь и отправляются не чаще одного в 2 секунды — лимит MAX не нарушается.

Шаг 6. Автоматические уведомления
Скрипт уже содержит обработчик OnTrade — при каждой сделке в QUIK на телефон автоматически уходит уведомление:

text
?? Прод **RIZ5** 5 @ 105250.00
?? Покуп **GAZP** 100 @ 198.50
Чтобы добавить свои триггеры, допишите в скрипт или в отдельный файл:

lua
-- Пример: уведомление при изменении цены
function OnQuote(class_code, sec_code)
    if sec_code == "RIZ5" then
        local ql = getQuoteLevel2(class_code, sec_code)
        if ql and #ql.bid > 0 then
            local bid_price = tonumber(ql.bid[#ql.bid].price)
            if bid_price and bid_price > 105000 then
                SendToMAX("RIZ5 выше 105000! Текущая bid: " .. bid_price, nil, 2)
            end
        end
    end
end

Шаг 7. Остановка
В окне Lua-скрипты нажмите Остановить.
Скрипт отправит в MAX сообщение: ?? MAX Bridge остановлен. (или с количеством неотправленных сообщений в очередях).
COM-объект освобождается, связь закрывается.

Типичные проблемы
Скрипт запущен, но /start не приходит:

Проверьте, что токен вставлен правильно, без пробелов.
Проверьте интернет-соединение на ПК.
В окне сообщений QUIK посмотрите ошибки — если WinHttpRequest не создан, возможно, COM заблокирован антивитором.
Сообщения приходят с задержкой:

Это нормально — rate limit 2 секунды. Если в очереди 5 сообщений, последнее уйдёт через 10 секунд.
Если задержка больше минуты — проверьте, не вырос ли ERR_COUNT (ошибки сети). При ошибках polling замедляется.
luacom не найден:

В некоторых сборках QUIK библиотека luacom не установлена по умолчанию.
Скачайте luacom.dll и положите в папку QUIK рядом с остальными .dll.
Кириллица в сообщениях ломается:

Скрипт отправляет в кодировке UTF-8.
Если QUIK отдаёт строки в Windows-1251 (что типично), кириллица может прийти кривой.
В этом случае нужно конвертировать текст перед отправкой — скажите, добавлю функцию перекодировки.
Если торгуете акциями, а не фьючерсами — замените FUTURES_CLIENT_HOLDING на нужную таблицу в build_reply для команды /positions.
--]]

Страницы: 1
Читают тему
Наверх