Блог им. Kostefan

Индикатор покупок и продаж для QUIK.

    • 26 октября 2026, 10:43
    • |
    • McDuck
  • Еще
Индикатор покупок и продаж для QUIK.

Индикатор delta volume для QUIK

Индикатор *delta volume предназначен для анализа объёма сделок, активности покупателей и продавцов и накопленной объёмной дельты. Он написан на Lua для терминала QUIK и использует информацию из таблицы сделок для выбранного инструмента.

Как работает индикатор

Главная задача индикатора — помочь понять, поддерживают ли сделки текущее движение цены или, наоборот, движение ослабевает.

 1. Объём покупок и продаж

По умолчанию зелёные столбцы показывают объём покупок, а красные столбцы ниже нуля — объём продаж. Высокие столбцы означают, что в соответствующем баре прошло больше сделок выбранного направления.

 2. Кумулятивная дельта — CumDelta

Это основной элемент индикатора. Она накапливает разницу между объёмом покупок и продаж с учётом настроек расчёта.

Если линия растёт, расчётный баланс смещается в сторону покупок; если снижается — в сторону продаж. При этом рост линии сам по себе не гарантирует дальнейшего роста цены.

 3. Общий объём — Volume

Показывает общий объём торгов за бар. В коде он отключён по умолчанию, но его можно включить настройкой showVolume = 1.

 4. Изменение открытого интереса — OI Delta

Дополнительная информация об изменении открытого интереса, полезная прежде всего для фьючерсов. По умолчанию выключена. Сама по себе эта величина не показывает однозначно, кто именно инициировал открытие позиции.

Как применять на графике

Например, цена растёт, а CumDelta тоже движется вверх — это подтверждает, что в расчётах индикатора преобладают покупки. Если же цена обновляет максимум, а кумулятивная дельта перестаёт расти или идёт вниз, можно внимательнее проверить, не ослабевает ли движение.

Такие расхождения стоит использовать как повод для дополнительного анализа, а не как самостоятельный сигнал на покупку или продажу.

Важные особенности исходного кода:

  • Кумулятивная дельта не сбрасывается автоматически в начале каждого дня: её значение зависит от обработанной истории графика.
  • Третья линия в коде подписана Delta, но фактически в неё выводится CumDelta.
  • По умолчанию направление сделок берётся из флагов сделок QUIK. Режим deltaType = 1, который по комментарию должен определять направление по изменению цены, в текущем коде реализован с ошибкой и требует исправления.

Как установить индикатор в QUIK

Установка пользовательских Lua-индикаторов выполняется через папку LuaIndicators и меню добавления индикатора на график

Создайте файл индикатора

Откройте приложенный файл Вставленный текст.txt, скопируйте весь код и вставьте его в текстовый редактор, например Блокнот.

Сохраните файл под именем delta_volume.lua. При сохранении выберите тип «Все файлы», чтобы не получилось имя delta_volume.lua.txt. Либо скачать готовый код из поста.

 Поместите файл в папку QUIK

Найдите папку, из которой работает ваш терминал, и откройте или создайте в ней папку: LuaIndicators

Пример пути: C:\QUIK\LuaIndicators\delta_volume.lua

Путь может отличаться в зависимости от брокера и установки терминала. Важно использовать именно рабочую папку вашего QUIK.

 Откройте QUIK и график

Если терминал был открыт во время копирования файла, перезапустите его, если индикатор не появляется в списке. Откройте график нужного инструмента, например Сбербанка.

 Добавьте индикатор

Нажмите правой кнопкой мыши по графику и выберите «Добавить график (индикатор)». В появившемся списке найдите *delta volume — именно так он называется в настройках кода.

Выберите его и нажмите «Добавить». Для объёмной гистограммы и кумулятивной дельты лучше использовать отдельную область графика, чтобы не смешивать их шкалу со шкалой цены.

 Настройте параметры

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

 Мой совет: сначала оставьте настройки по умолчанию. Посмотрите, как меняется CumDelta при росте и падении цены, и сравнивайте её с гистограммой покупок и продаж. Если хотите видеть единую положительную или отрицательную дельту каждого бара вместо отдельных объёмов покупок и продаж, установите showBarDelta = 1.

Обратите внимание: если индикатор появляется в списке, но не отображает данные, стоит проверить доступность таблицы сделок в QUIK. Таблица обезличенных сделок должна быть открыта  по выбранному инструменту и сделки в ней должны отображается. Если нет в ней сделок то обратитесь к вашему брокеру, возможно они не подключены. Это индикатор анализа сделок, а не готовая торговая стратегия.


Индикатор покупок и продаж для QUIK.


--[[
	
	https://smart-lab.ru/my/Kostefan/
]]

local logfile = nil
-- logfile				= io.open(_G.getWorkingFolder().."\\LuaIndicators\\volume.txt", "w")

local SEC_CODE 		= ""
local CLASS_CODE 	= ""
local myVol 		= function() end
local tradeDate
local floor 		= math.floor
local ceil 			= math.ceil
local os_time		= os.time
local os_date		= os.date
local string_find	= string.find
local bit_test		= _G.bit.test
local max_errors	= 10
local V     		= _G['V']
local T     		= _G['T']
local Size  		= _G['Size']
local message		= _G.message
local CandleExist	= _G.CandleExist
local SetValue		= _G.SetValue
local getItem		= _G.getItem
local getNumberOf	= _G.getNumberOf
local SearchItems	= _G.SearchItems
local call_count    = 0
local interval		= 60

local cache_VolAsk
local cache_VolBid
local OIDelta

_G.Settings = {
	Name 				= "*delta volume",
	-- Выводить объем баров
	showVolume			= 0,
	-- Показывать объемную дельту отдельного бара, вместо данных покупок/продаж
	showBarDelta		= 0,
	-- Признак инверсии значений объемной дельты. Продажа - отрицательные значения. Покупка -положительные значения
	inverse 			= 1,
	-- Признак расчета объемной дельты не по показателям сделки, а по количеству сделок за интервал времени
	-- Если установлено 1, то объемная дельта будет сформирована как разница числа сделок за интервал времени
	CountQuntOfDeals 	= 0,
	-- Признак расчета объемной дельты по количеству (объему) сделки
	-- Значения:
	-- 0 - расчет объемной дельты ведется по сумме (в деньгах) сделки
	-- 1 - расчет объемной дельты ведется по количеству сделки
	sum_quantity		= 1,
	-- Показывать объемную дельту
	showDelta			= 1,
	-- Показывать кумулятивную объемную дельту
	showCumDelta		= 1,
	-- Показывать дельту ОИ
	showOIDelta			= 0,
	-- Масштабный коэффициент вывода кумулятивной объемной дельты.
	-- Значение будет умножено на коэффициено, чтобы соотносилось с значением дельты
	delta_koeff 		= 0.1,
	-- Фильтр объема сделки при расчете объемной дельты.
	-- Задается как список значений, разделенных ;
	-- Пример: 1;2;5;10;100;
	-- Если задан, то при расчете дельты будут учитываться сделки толкьо указанных объемов
	dealFilter 			= '',
    -- Вариант определения направления сделки для расчета дельты
    -- 0 - направление из ТОС
    -- 1 - направление считается как дельта от прошлой цены. Если цена снизилась, то продажа, если повысилась, то покупка
	deltaType			= 0,
	line =
    {
        {
         Name 	= "Sell",
         Color 	= _G.RGB(255,128,128),
         Type 	= _G.TYPE_HISTOGRAM,
         Width 	= 3
        },
        {
         Name 	= "Buy",
         Color 	= _G.RGB(120,220,135),
         Type 	= _G.TYPE_HISTOGRAM,
         Width 	= 3
        },
		{
         Name 	= "Delta",
         Color 	= _G.RGB(0,0,0),
         Type 	= _G.TYPE_LINE,
         Width 	= 1
        },
		{
         Name 	= "OI Delta",
         Color 	= _G.RGB(0,0,0),
         Type 	= _G.TYPE_HISTOGRAM,
         Width 	= 2
        },
		{
         Name 	= "Volume",
         Color 	= _G.RGB(0,128, 255),
         Type 	= _G.TYPE_HISTOGRAM,
         Width 	= 3
        }
    }
}

-- ѕользовательcкие функции
local function myLog(text)
	if not logfile then return end
	logfile:write(tostring(os_date("%c", os_time())).." "..text.."\n");
	logfile:flush();
end

local function round(num, idp)
    if num then
        local mult = 10^(idp or 0)
        if num >= 0 then
            return floor(num * mult + 0.5) / mult
        else
            return ceil(num * mult - 0.5) / mult
        end
    else
        return num
    end
end

----------------------------------------------------------
---@param class_code string
---@param sec_code string
local function FilterTableLine(class_code, sec_code)

        return  class_code == CLASS_CODE and
                sec_code == SEC_CODE
end

local function filterQuantity(qty, filterString)
	if (filterString or '') == '' then
		return true
	end
	if string_find(filterString, tostring(qty)..";") ~= nil then
		return true
	end
	return false
end

local function ReadTradesProcessor(inverse, sum_quantity, filterString, CountQuntOfDeals, deltaType)

	local last_price = 0
	local last_oi
	local cache_search
	local cache_index
	local cache_i = 0

	myLog("ReadTradesProcessor init"..' inverse '..tostring(inverse)..' sum_quantity '..tostring(sum_quantity))

	return function(index, timeFrom, timeTo, firstindex)

		local status,res = pcall(function()
			-- Перебирает все сделки в таблице "Сделки"

			local all_trades_count = getNumberOf("all_trades")
			--myLog("firstindex "..tostring(firstindex).." all_trades_count "..tostring(all_trades_count).." cache_index "..tostring(cache_index))

			if all_trades_count~=0 and timeTo ~= nil then
				local trade 	= getItem("all_trades", 0)
				local datetime 	= os_time(trade.datetime)
				if datetime > timeTo then
					return firstindex-1
				end
			end

			local t1 = cache_search
			if not cache_search or index >= cache_index then
				t1 = SearchItems("all_trades", firstindex, all_trades_count-1, FilterTableLine, 'class_code,sec_code')
				cache_search = nil
				cache_i 	 = nil
				-- myLog("SearchItems firstindex "..tostring(firstindex).." #t1 "..tostring(t1 and #t1))
			end
			if firstindex == 0 then
				cache_search 	= t1
				cache_index 	= Size()-1
			end

			if t1 then

				for i = (cache_i or 1), #t1, 1 do

					local trade = getItem("all_trades", t1[i])

					if trade then

						-- if not cache_search then
						-- 	myLog("trade ".."i("..i..")".."SEC_CODE: "..trade.sec_code.."; deal time "..os_date("%c", os_time(trade.datetime)).."; price "..tostring(trade.price).."; vol "..tostring(trade.qty).."; itsSell "..tostring(itsSell).."; cache_VolAsk: "..tostring(cache_VolAsk[index]).."; cache_VolBid: "..tostring(cache_VolBid[index]));
						-- end
						local datetime = os_time(trade.datetime)
						if datetime >= timeFrom then
							if timeTo == nil or datetime < timeTo then
								local value
								if (filterString or '') == '' or filterQuantity(trade.qty, filterString) then
									if CountQuntOfDeals == 1 then
										value = 1
									elseif sum_quantity == 0 then
										value = trade.value
									else
										value = trade.qty
									end
									local itsSell = bit_test(trade.flags, 0)
									if deltaType == 1 then
										if last_price~=0 and trade.price~=last_price then
											trade.itsSell = trade.price < last_price
										end
										last_price = trade.price
									end
									if last_oi then
										OIDelta[index] = OIDelta[index] + (trade.open_interest - last_oi)
									end
									last_oi = trade.open_interest
									if itsSell then --продажа
										if inverse == 0 then
											cache_VolAsk[index] = cache_VolAsk[index] + value
										else
											cache_VolAsk[index] = cache_VolAsk[index] - value
										end
									else
										cache_VolBid[index] = cache_VolBid[index] + value
									end
									-- if not cache_search then
									-- 	myLog(" ---- count deal ".."i("..t1[i]..")".."SEC_CODE: "..trade.sec_code.."; deal time "..os_date("%c", os_time(trade.datetime)).."; price "..tostring(trade.price).."; vol "..tostring(trade.qty).."; itsSell "..tostring(itsSell).."; cache_VolAsk: "..tostring(cache_VolAsk[index]).."; cache_VolBid: "..tostring(cache_VolBid[index]));
									-- end
								end
							else
								if cache_search then cache_i = i-1 end
								return t1[i-1] or firstindex-1
							end
						end
					end
				end
				return t1[#t1]
			end
			return firstindex-1
		end)
		if not status then
			myLog('Error ReadTrades: '..tostring(res))
			return firstindex-1
		end
		return res
	end
end

local function FindExistCandle(index)
	local out = index
	while not CandleExist(out) and out <= Size() do
		out = out +1
	end
	return out
end

local function Vol(Fsettings)

	local Delta				= {}
	local CumDelta			= {}
	local LastReadDeals 	= -1
	local last_index		= Size()
	local errors			= 0
	local ReadTrades		= function() return -1 end
	local error_log			= {}
	local max_errors_reach

	Fsettings 				= Fsettings or {}
	local showBarDelta 		= Fsettings.showBarDelta or 0
	local showDelta 		= Fsettings.showDelta or 0
	local showCumDelta 		= Fsettings.showCumDelta or 0
	local showOIDelta 		= Fsettings.showOIDelta or 0
	local showVolume 		= Fsettings.showVolume or 0
	local inverse 			= Fsettings.inverse or 0
	local CountQuntOfDeals 	= Fsettings.CountQuntOfDeals or 0
	local sum_quantity 		= Fsettings.sum_quantity or 1
	local delta_koeff 		= Fsettings.delta_koeff or 0
	local deltaType 		= Fsettings.deltaType or 0
	local filterString 		= Fsettings.dealFilter or ''

	return function(index)

		local status, res = pcall(function()

			if index == 1 then
				-- myLog("tradeDate "..os_date("%c",os_time(tradeDate))..' index '..tostring(index)..' call_count '..tostring(call_count))
				last_index				= Size()
				cache_VolBid			= {}
				cache_VolAsk			= {}
				Delta					= {}
				Delta[index]			= 0
				CumDelta				= {}
				CumDelta[index]			= 0
				OIDelta					= {}
				OIDelta[index]			= 0
				LastReadDeals			= -1
				ReadTrades				= call_count == 0 and ReadTrades or ReadTradesProcessor(inverse, sum_quantity, filterString, CountQuntOfDeals, deltaType)
				call_count				= call_count + 1
			end

			Delta[index] 			= Delta[index-1] or 0
			CumDelta[index] 		= CumDelta[index-1] or 0
			OIDelta[index] 			= OIDelta[index] or 0
			cache_VolAsk[index]		= cache_VolAsk[index] or 0
			cache_VolBid[index]		= cache_VolBid[index] or 0

			if not CandleExist(index) then
				return nil
			end
			local timeFrom 	= os_time(T(index))
			local timeTo 	= round(os_time(T(index)) + interval*60, 0)
			if index < Size() then
				local nextCandle = FindExistCandle(index+1)
				if nextCandle < Size()+1 then
					timeTo = T(nextCandle)
					if tradeDate.year ~= timeTo.year or tradeDate.month ~= timeTo.month or tradeDate.day ~= timeTo.day then
						if showVolume == 1 then
							return nil, nil, nil, V(index)
						else
							return nil, nil, nil, nil
						end
					end
					timeTo = os_time(timeTo)
				end
			end

			local function calc_delta(ind)
				local localDelta
				if inverse == 0 then
					localDelta = cache_VolBid[ind]-cache_VolAsk[ind]
				else
					localDelta = cache_VolBid[ind]+cache_VolAsk[ind]
				end
				Delta[ind] 		= localDelta
				CumDelta[ind] 	= CumDelta[ind] + localDelta*delta_koeff
			end

			--myLog("------------------------------------------------------------")
			--myLog("OnCalc() ".."CandleExist("..index.."): "..tostring(CandleExist(index)).."; T("..index..") "..os_date("%c",timeFrom).."; LastReadDeals "..tostring(LastReadDeals));
			--myLog("timeFrom "..os_date("%c",timeFrom))
			--myLog("timeTo "..os_date("%c", timeTo))
			if last_index < Size() then
				LastReadDeals = ReadTrades(index-1, os_time(T(last_index)), timeFrom, LastReadDeals+1)
				calc_delta(last_index)
				SetValue(last_index, 1, showDelta == 1 and (showBarDelta == 0 and cache_VolAsk[last_index] or (Delta[last_index] < 0 and Delta[last_index])))
				SetValue(last_index, 2, showDelta == 1 and (showBarDelta == 0 and cache_VolBid[last_index] or (Delta[last_index] > 0 and Delta[last_index])))
				SetValue(last_index, 3, showCumDelta == 1 and CumDelta[last_index])
				SetValue(last_index, 4, showOIDelta == 1 and OIDelta[last_index])
				SetValue(last_index, 5, showVolume == 1 and V(last_index))
				last_index 				= Size()
				OIDelta[index - 2] 		= nil
				Delta[index - 2] 		= nil
				cache_VolAsk[index - 2]	= nil
				cache_VolBid[index - 2]	= nil
			end
			LastReadDeals = ReadTrades(index, timeFrom, timeTo, LastReadDeals+1)
			--myLog("LastReadDeals "..tostring(LastReadDeals))
			-- myLog(' index '..tostring(index)..' '..os_date('%d.%m.%Y %H:%M:%S', timeFrom).." cache_VolAsk "..tostring(cache_VolAsk[index])..", cache_VolBid "..tostring(cache_VolBid[index]))
			--myLog('all vol '..tostring(cache_VolAsk[index] + cache_VolBid[index]))

			calc_delta(index)

		end)
		if not status then
			errors = errors + 1
			if errors > max_errors and not max_errors_reach then
				message(_G.Settings.Name ..': Слишком много ошибок при работе индикатора.')
				max_errors_reach = true
			end
            if not error_log[tostring(res)] then
                error_log[tostring(res)] = true
                myLog('Error CalcFunc: '..tostring(res))
                message('Error CalcFunc: '..tostring(res))
            end
            return nil
		end

		----myLog("Delta "..tostring(Delta[index]))
		return 	showDelta == 1 and (showBarDelta == 0 and cache_VolAsk[index] or (Delta[index] < 0 and Delta[index])),
				showDelta == 1 and (showBarDelta == 0 and cache_VolBid[index] or (Delta[index] > 0 and Delta[index])),
				showCumDelta == 1 and CumDelta[index],
				showOIDelta == 1 and OIDelta[index],
				showVolume == 1 and V(index)
	end
end

function _G.OnCalculate(index)
	if index == 1 then
		local DSInfo 	= _G.getDataSourceInfo()
		SEC_CODE 		= DSInfo.sec_code
		CLASS_CODE 		= DSInfo.class_code
		interval 		= DSInfo.interval
		tradeDate 		= _G.getTradeDate()
		--myLog("OnCalculate 1 "..tostring(SEC_CODE).."|"..tostring(CLASS_CODE)..", tradeDate "..tostring(tradeDate)..' Size '..tostring(Size())..' interval '..tostring(interval))
	end
	return myVol(index)
end

function _G.Init()
	call_count 	= 0
	myVol 		= Vol(_G.Settings)
	return 5
end

function _G.OnChangeSettings()
	myLog("OnChangeSettings")
    _G.Init()
end

function _G.OnDestroy()
	if logfile then logfile:close() end
end
Данная публикация является личным мнением автора. Мнение владельца сайта может не совпадать с мнением автора.
  • обсудить на форуме:
  • QUIK
321 | ★6
#18 по плюсам, #23 по комментариям
2 комментария

Как работает индикатор

Главная задача индикатора — помочь понять, поддерживают ли сделки текущее движение цены или, наоборот, движение ослабевает.

Полная феерическая куета для лудоманов и нового мясца! Это это просто смазка ММ и что бы ее понять, надо ни «складывать и вычитать», а понимать работу ММ. 
За сам индикатор на луа — спасибо! Сейчас это делает клод за нормальный рабочий день. Но все же — спасибо.
avatar
Head of Algonaft'$, Пусть люди двигаются в изучении не только машек. Может кому то поможет.
avatar

Читайте на SMART-LAB:
Фото
Операционные результаты ПАО «Группа «Русагро» за 3 квартал 2026 года
«Русагро» делится результатами начала нового сельскохозяйственного сезона. В 3-ем кв. 2026 года консолидированная выручка «Русагро»...
Фото
Застройщики сохраняют финансовую устойчивость. Взгляд Банка России
Банк России 23 октября 2026 года сохранил ключевую ставку на уровне 14 %. На пресс‑конференции председатель ЦБ Эльвира Набиуллина сделала несколько...
⚡️ Завтра СД Займера рассмотрит дивиденды за III квартал
27 октября Совет директоров Займера рассмотрит вопрос о рекомендации размера дивидендов по итогам III квартала 2026 года. Размер рекомендованных...
Фото
📈MRKP: +33% за 3,5 месяца! Пора пересматривать оценку?
7 июля 26г. я повысил рейтинг компании Россети Центр и Приволжье с 3 до 4 по цене 0,446 руб./акц. и вот что я писал тогда:

теги блога McDuck

....все тэги



UPDONW
Новый дизайн