Блог им. turbo_pascal
Алгоритм данной торговли был описан уважаемым Гном (https://smart-lab.ru/blog/499606.php) и, поскольку я являюсь любителем различных теорий Мартингейла и усреднения, написал робота по этой стратегии.
Подробно на алгоритме останавливаться не буду — читайте по ссылке у Гнома, там очень хорошо всё расписано.
Здесь — немного измененная реализация. Отличие в том, что позиции открываются не через равные промежутки цены, а чуть шире: еще должно прийти хотя бы минимальное подтверждение, что дальше не полетит (в данном случае использован вход обратно в канал Боллинджера, но это несложно поменять на что угодно).
Если полетит против нас вертикально, мы хотя бы не будет бессмысленно открывать кучу сделок на мгновенной длинной вертикальной палке.
Итак, представляю: «Судак-Тудак» Универсальный (одновременно для акций и фьючерсов).

Если хотите добавить инструменты (а они добавляются в массив aTickerList), не забудьте вписать их данные в массивы:
* aLotSize — кол-во лотов на сделку. Обычно 1 (помните, что в акциях — кол-во акций в лоте у каждого свой. Так что лучше просто оставьте 1).
* aProskalzivanie — для сделки. Примерно пол-процента от цены.
* aAccountCode — могут отличаться для фьючерсов и акций.
* aClassCode — TQBR для фьючерсов, SPBFUT — для акций.
ВНИМАНИЕ! Для каждого инструмента должен быть открытый график с ценой и болосой Боллиинджера.
Идентификатор цены: имя тикера + "_Price_Sudak".
Идентификатор Боллинджера: имя тикера + "_BB_Sudak".
Без прописывания идентификаторов работать не будет (CreateDataSource я не использую и не хочу, пусть данные цены и индикаторов считаются на графике, а я оттуда заберу готовое).
Алгоритм (работает по каждому инструменту из перечисленных в aTickerList).
Если Цена снизилась от предыдущей покупки на покупку более чем на StepSize,
и при этом мы вошли снизу внутрь Боллинджера, то Докупка aLotSize.
Наоборот — сброс aLotSize.
aStepSize не играет роли в сильном боковике — мы продаем и покупаем при каждом касании краев Боллинджера (но не менее чем aFlatSize).
aStepSize нужен только для растягивания усреднения докупки и растягивании усреднения сдачи.
Параметры последней следки: направление, цена последней сделки, и объем позиции — хранятся в файликах.
Имя Тикера + "_LastPrice" — цена последней сделки.
Имя Тикера + "_LastDirection" — направление последней сделки.
Имя Тикера + "_Volume" — текущий объем позиции.
Робот выставляет сделки по рынку и НЕ проверяет OnOrder и OnTrade, поэтому в файлик "_LastPrice" может быть записана чуть неточная цена (с учетом проскальзывания).
Файлики можно править вручную (хотя нужно ли).
Только в лонг. Если условия выполняются, но продавать нечего (_Volume = 0) — не продаем.

Более подробно об остальных параметрах — в самом скрипте. Комментариев там более чем достаточно.
Как сделать так, чтобы данный робот не сливал от слова совсем? Правильно: не суйте туда много с самого начала. Это инвестиционный робот, он не предназначен для ежедневной прибыли. Он предназначен для пощипывания рынка при практически любом движении, в расчете на то, что если случится вселенский песец, мы уже отработали бы и вывели все что надо.
Если все таки ожидаете роста актива, купите стартовый лот сами, сколько хотите. Пропишите только вручную в файлы _LastPrice и _Volume цену покупки и объем (в лотах, а не в штуках), и ждите роста, подсекая колебания в свою пользу.
Стопов тут нет, используется усреднение (и это основной механизм). Перед тем, как запускать его по отдельному инструменту, необходимо расчитать, на сколько данный инструмент может упасть вниз и что мы при этом будем делать.
Например, сейчас Газпром торгуется по 160 рублей. Выставляем StepSize = 2 рубля и начинаем с одного лота. Итого, мы можем открыть 80 лотов со средней ценой 80 рублей, даже если цена упадет до нуля. У нас уйдет на это меньше 70 тысяч рублей (в реальности намного меньше, т.к. открываем мы сделки не через 2 рубля, а шире).
Теперь остается посчитать, каковы шансы Газпрому стоить ноль, и шансы, что он опять будет 10 лет болтаться в боковике, наращивая нам прибыль, а мы будем стричь дивиденды на всю открытую позицию, и покупать-продавать на каждом чихе.
Скрипт ниже.
Всем кто будет пользоваться: будут лезть ошибки — пишите.
Уважаемым алготрейдерам: если предложите оптимизацию кода и логики, буду благодарен (по программированию на qlua я пока не большой спец), поправлю и выложу новую версию.
p.s.
Ничего не продаю, семинаров не веду, каналов нет, в ДУ не беру.
-- Судак-Тудак Универсальный (для акций и фьючерсов). Версия 0.17
-- "Turbo Pascal" © 2019. Это ник такой. Текст на Луа.
-- Если хотите добавить инструменты (а они добавляются в массив aTickerList), не забудьте вписать их данные в массивы
-- * aLotSize - кол-во лотов на сделку. Обычно 1 (помните, что в акциях - кол-во акций в лоте у каждого свой. Так что лучше просто оставьте 1).
-- * aProskalzivanie - для сделки. Примерно пол-процента от цены.
-- * aAccountCode - могут отличаться для фьючерсов и акций.
-- * aClassCode - TQBR для фьючерсов, SPBFUT - для акций.
-- ВНИМАНИЕ! Для каждого инструмента должен быть открытый график с ценой и болосой Боллиинджера.
-- Идентификатор цены: имя тикера + "_Price_Sudak".
-- Идентификатор Боллинджера: имя тикера + "_BB_Sudak".
-- Без прописывания идентификаторов работать не будет.
-- Алгоритм (работает по каждому инструменту из перечисленных в aTickerList).
-- Если Цена снизилась от предыдущей покупки на покупку более чем на StepSize,
-- и при этом мы вошли снизу внутрь Боллинджера, то Докупка aLotSize.
-- Наоборот - сброс aLotSize.
-- aStepSize не играет роли в сильном боковике - мы продаем и покупаем при каждом касании краев Боллинджера.
-- Минимальный диапазон "купи-продай" - aFlatSize.
-- aStepSize нужен только для растягивания усреднения докупки и растягивании усреднения сдачи.
-- Параметры последней следки: направление, цена последней сделки, и объем позиции - хранятся в файликах.
-- Имя Тикера + "_LastPrice" - цена последней сделки.
-- Имя Тикера + "_LastDirection" - направление последней сделки.
-- Имя Тикера + "_Volume" - текущий объем позиции.
-- Робот выставляет сделки по рынку, поэтому в файлик "_LastPrice" может быть записана
-- чуть неточная цена (с учетом проскальзывания).
-- Файлики можно править вручную (хотя нужно ли).
-- Только в лонг. Если условия выполняются, но продавать нечего - не продаем.
-- По расчетам: Если докупать Сбер(ао) с цены 250, то чтобы набрать позу 50 лотов, упав до 150, достаточно капитала в 100т.р.
-- Газпром упадет с 160 до 60 - для удержания достаточно 50т.р.
-- На "рехеджах" (каждая пара бай-селл) еще и заработать можно. Дивы - тоже в нашу пользу.
-- ************************ СЕКЦИЯ ОБЩИХ ПАРАМЕТРОВ ****************************
CLIENT_CODE = "сюдой пиши своё" -- общий код для акций и фьючерсов.
LogFileName = "c:\\Log\\sudaktudak_log.txt" -- Технический лог.
ParamPath = "c:\\SudakTudak\\" -- здесь хранятся файлики с параметрами. Три файла на каждый инструкмент.
SdelkaLog = "c:\\Log\\sudaktudak_sdelki.txt" -- Лог сделок. Сюда пишутся ТОЛЬКО сделки.
SleepDuration = 10; -- отдыхаем 10 секунд. Слишком часто не надо молотить.
DemoMode = false -- Включите, чтобы начать "боевые" сделки. Если = false, сделок не будет, просто запишет в лог.
-- ************************ СЕКЦИЯ МАССИВОВ ДЛЯ ИНСТРУМЕНТОВ ************************
aTickerList = {"SBER", "GZM9"}; -- сюда массив инструментов. Не забывайте перекладывать фьючерсы!!!
-- А при перекладывании фьючерсов не забывайте менять код как здесь, так и в следующих массивах.
-- Следующие массивы должны иметь значения для каждого инструмента из aTickerList
aClassCode = {SBER="TQBR", GZM9="SPBFUT"} -- TQBR для акций, SPBFUT для фьючерсов.
aAccountCode = {SBER="сюды своё", GAZP="сюды своё", GZM9="сюды своё"} -- может отличаться для акций и фьючерсов.
aLotSize = {SBER=1, GAZP=1, GZM9=1}; -- Массив лотов для покупки.
aStepSize = {SBER=2, GAZP=2, GZM9=200}; -- Шаг Цены.
aFlatSize = {SBER=0.5, GAZP=0.5, GZM9=50}; -- Шаг Цены.
aProskalzivanie = {SBER=1,GAZP=1, GZM9=100}; -- Проскальзывание при сделке.
is_run=true
function main()
while is_run do
for k,v in pairs(aTickerList) do
Obrabotka(v,k);
end;
sleep(SleepDuration*1000) -- Отдыхаем SleepDuration секунд.
end
end
function GetLastPrice(TickerName, CandleType)
-- Берем цену из графика. CreateDataSource пока не используем, т.к. при необходимости модификации
-- алгоритма, хотим легко добавлять индикаторы.
-- Плюс меньше зависим от коннекта - графики всегда с нами.
local NL=getNumCandles(TickerName.."_Price_Sudak")
tL, nL, lL = getCandlesByIndex (TickerName.."_Price_Sudak", 0, NL-1, 1) -- last свеча
local aCurrentPrice=tL[0].close -- получили текущую цену (ЦПС)
if CandleType=="OPEN" then aCurrentPrice=tL[0].open end;
if CandleType=="HIGH" then aCurrentPrice=tL[0].high end;
if CandleType=="LOW" then aCurrentPrice=tL[0].low end;
return aCurrentPrice
end
function GetBollinger(TickerName, LineCode)
-- получаем текущие значения Боллинлжера.
-- LineCode может иметь значения: "High", "Middle", "Low"
local NbbL=getNumCandles(TickerName.."_BB_Sudak")
tbbL, nbbL, lbbL = getCandlesByIndex (TickerName.."_BB_Sudak", 0, NbbL-1, 1) -- last свеча, средняя линия Боллинджера
iBB_Local_Middle = tbbL[0].close -- тек значение средней BB Local
tbbL, nbbL, lbbL = getCandlesByIndex (TickerName.."_BB_Sudak", 1, NbbL-1, 1) -- last свеча, верхняя линия Боллинджера
iBB_Local_High = tbbL[0].close -- тек значение верхней BB Local
tbbL, nbbL, lbbL = getCandlesByIndex (TickerName.."_BB_Sudak", 2, NbbL-1, 1) -- last свеча, нижняя линия Боллинджера
iBB_Local_Low = tbbL[0].close -- тек значение нижней BB Local
if LineCode == "High" then return iBB_Local_High end
if LineCode == "Middle" then return iBB_Local_Middle end
if LineCode == "Low" then return iBB_Local_Low end
end
function PriceCrossMAToUp(TickerName)
-- Функция возвращает TRUE, если пересекли среднюю линию Боллинджера снизу вверх
if GetLastPrice(TickerName, "OPEN")<GetBollinger(TickerName, "Middle")
and GetLastPrice(TickerName, "LAST")>GetBollinger(TickerName, "Middle")
then return true
else return false
end;
end
function PriceCrossMAToDown(TickerName)
-- Функция возвращает TRUE, если пересекли среднюю линию Боллинджера снизу вверх
if GetLastPrice(TickerName, "OPEN")>GetBollinger(TickerName, "Middle")
and GetLastPrice(TickerName, "LAST")<GetBollinger(TickerName, "Middle")
then return true
else return false
end;
end
function PriceEnterToBollingerFromDown(TickerName)
-- Функция возвращает TRUE, если пересекли нижнюю линию Боллинджера снизу вверх
-- (то есть вошли внутрь канала Боллинджера снизу).
if GetLastPrice(TickerName, "LOW")<GetBollinger(TickerName, "Low")
and GetLastPrice(TickerName, "LAST")>GetBollinger(TickerName, "Low")
then return true
else return false
end;
end
function PriceEnterToBollingerFromUp(TickerName)
-- Функция возвращает TRUE, если пересекли верхнюю линию Боллинджера сверху вниз
-- (то есть вошли внутрь канала Боллинджера сверху).
if GetLastPrice(TickerName, "HIGH")>GetBollinger(TickerName, "High")
and GetLastPrice(TickerName, "LAST")<GetBollinger(TickerName, "High")
then return true
else return false
end;
end
--Функция Обработки отдельного тикера
function Obrabotka(sTickerName, sNum)
-- Теперь откываем файлы с нашим тикетом, и читаем оттуда значения: LastPrice, LastDirection, Объем Позы.
LastPrice = tonumber(GetValueFromFile(ParamPath..sTickerName.."_LastPrice.txt"));
LastDirection = GetValueFromFile(ParamPath..sTickerName.."_LastDirection.txt");
iVolume = tonumber(GetValueFromFile(ParamPath..sTickerName.."_Volume.txt"));
local CurrentPrice=GetLastPrice(sTickerName) -- вытаскиваем из графика текущую цену.
-- Логируем текущую и последнюю цены и разницу между ними.
WLOG(sTickerName.. " Current="..CurrentPrice.. " Last="..LastPrice.." Razn = "..(CurrentPrice-LastPrice).. " StepSize = "..aStepSize[sTickerName].." Vol="..iVolume.." LastDir="..LastDirection);
-- Теперь проверяем, не надо ли докупиться?
if (((CurrentPrice<LastPrice-aStepSize[sTickerName]) or (CurrentPrice<LastPrice-aFlatSize[sTickerName] and LastDirection=="S")) or (iVolume==0)) and PriceEnterToBollingerFromDown(sTickerName) then
-- Подсекаем или Начинаем
WLOG("Podsekaem or Nachinaem");
if (DoFire(sTickerName, "B", CurrentPrice) == "") then
iVolume = tonumber(iVolume)+aLotSize[sTickerName];
SetValueToFile(ParamPath..sTickerName.."_LastPrice.txt", CurrentPrice)
SetValueToFile(ParamPath..sTickerName.."_LastDirection.txt", "B")
SetValueToFile(ParamPath..sTickerName.."_Volume.txt", tostring(iVolume))
LastPrice = CurrentPrice; -- чтобы не продать сразу на следующем условии.
end
end
-- Теперь проверяем, не надо ли немного сбросить?
if ((CurrentPrice>LastPrice+aStepSize[sTickerName]) or (CurrentPrice>LastPrice+aFlatSize[sTickerName] and LastDirection=="B")) and PriceEnterToBollingerFromUp(sTickerName) and (iVolume>0) then
-- Сдаем
WLOG("Sdaem");
if (DoFire(sTickerName, "S", CurrentPrice) == "") then
iVolume = tonumber(iVolume)-aLotSize[sTickerName];
SetValueToFile(ParamPath..sTickerName.."_LastPrice.txt", CurrentPrice)
SetValueToFile(ParamPath..sTickerName.."_LastDirection.txt", "S")
SetValueToFile(ParamPath..sTickerName.."_Volume.txt", tostring(iVolume))
end
end
end;
function DoFire(SEC_CODE, p_dir, p_price) -- Функция - СДЕЛКА ПО РЫНКУ!
if p_dir == "B" then AAA = 1 else AAA = -1 end
t = {
["CLASSCODE"]=aClassCode[SEC_CODE],
["SECCODE"]=SEC_CODE,
["ACTION"]="NEW_ORDER", -- новая сделка.
["ACCOUNT"]=aAccountCode[SEC_CODE],
["CLIENT_CODE"]=CLIENT_CODE,
["TYPE"]="L", -- "M" "L". По M давал ошибку на TQBR.
["OPERATION"]=p_dir, -- направление сделки, "B" или "S"
["QUANTITY"]=tostring(aLotSize[SEC_CODE]), -- объем, (акции - в лотах, а не штуках).
["PRICE"]=tostring(p_price+(aProskalzivanie[SEC_CODE]*AAA)), -- цену лимитки ставим для мгновенного исполнения.
["TRANS_ID"]="1"
}
if DemoMode==false then -- Если всё по серьезному, то ...
res1 = sendTransaction(t) -- ... передаем сделку по рынку.
end
if (res1~="") then -- Ошибочка вышла. Логируем ошибку.
WLOG("SendTransaction Error = "..res1);
end
local l_file1=io.open(SdelkaLog, "a")
l_file1:write(os.date()..";SECCODE="..SEC_CODE..";PRICE="..p_price..";DIR="..p_dir.."\n")
l_file1:close()
return res1
end
function GetValueFromFile(FileName) -- Читаем параметр из файла.
local f = io.open(FileName, "r");
if f == nil then -- если файла нет, но создаем пустой.
f = io.open(FileName,"w");
DefaultValueForFile = "0" -- по умолчанию пишем нуль.
-- Для LastDirection надо бы писать не нуль, а "B", но пусть будет нуль, т.к.
-- этого условия достаточно для открытия начальной сделки.
f:write(DefaultValueForFile)
f:close();
-- Открывает уже гарантированно существующий файл в режиме "чтения/записи"
f = io.open(FileName, "r");
end;
aValue = f:read("*l")
f:close()
return aValue
end
function SetValueToFile(FileName, aValue) -- Пишем параметр в файл.
local ff=io.open(FileName, "w") -- используем "w", а не "a", чтобы перезаписать существующий.
ff:write(aValue)
ff:close()
end
function OnStop(stop_flag)
is_run=false
end
function WLOG(st) -- Универсальная функция записи в лог.
local l_file=io.open(LogFileName, "a") -- используем "a", чтобы добавить новую строку.
l_file:write(os.date().." "..st.."\n")
l_file:close()
end
Чувак, это что такое? Как ты мог? Какая ещё нах Obrabotka?
Vodka
Balalaika
Kalash
Putin
и внимание...
Obrabotka!
Скрипт то для русскоязычной аудитории.
Что не так?
2) Проверяйте, чтобы папки, указанные для логов и настроек, существовали. Файлы робот создаст сам, а папки должны быть.
И хотя я пока занят немного другим, без сожаления отдал весь остаток тимофейчиков, пусть и крохотный, зато от души — ибо бальзам на сердце :)
Набор позиции и сдача оптом — хочу отдельный скрипт накидать. Но это будет уже классический безжалостный (для депозита
На одном форексном форуме годами всем миром пилят «иланы» и «2-side-стохастики». Народу не надоедает :)
Они, конечно, сольют, но в процессе брейншторма идей там очень много полезных генерится.
Попозже, наверно, на гитхабе буду выкладывать.
Продает он не по уровням, а по входу в ББ сверху, но чтобы было шире чем aStepSize.
Syntax error while compiling C:\SudakTudak\sudak.lua: C:\SudakTudak\sudak.lua:1: unexpected symbol near 'п'
К тому же, откуда «Compiling»? Вы в чем его запускаете? К квике или в обычном Lua?
Сам скопировал, проверил, работает.
monosnap.com/file/oRG1CDh2UnCEw9sVM5BuaWWpxaRceT
www.filedropper.com/sudaktudak1
у меня вот как раз по Сберу так и стоИт.
04/23/19 14:28:02 SBER Current=236.22 Last=236.28 Razn = -0.060000000000002 StepSize = 2 Vol=-1 LastDir=S
04/23/19 14:28:02 GAZP Current=163.19 Last=163.1 Razn = 0.090000000000003 StepSize = 1 Vol=3 LastDir=B
04/23/19 14:28:02 MTSS Current=260.75 Last=0 Razn = 260.75 StepSize = 3 Vol=0 LastDir=0
Проверяйте: был шорт (и вообще открытая позиция) до запуска?
У меня обычно робот включается минут за 15 до начала рынка, и в первую минуту уже иногда делает сделку.
Но потом сутками молотит и не отваливается.
И что можно с логом на 35 МБ делать? :)
Turbo Pascal, привет! Перепроверил, график подписан SBER_Price_Sudak, боллинджер SBER_BB_Sudak.
Настраиваю, соответсвенно Сбер, пока один
И тоже на 71 строке выдает «attempt to index a nil value (field 'integer index')»
Может что-то в коде обновили за 5 лет?
И еще вопрос сразу - aAccountCode — это вообще что? Что писать?
ВСЕХ благодарю!
2 дня тыкался — как сюда вопрос написал — так сразу все запустилось
Видимо аура сайта сработала![]()
Turbo Pascal, в комментах к коду написано:
«DemoMode = false — Включите, чтобы начать „боевые“ сделки. Если = false, сделок не будет, просто запишет в лог.»
Смотрю по коду:
if DemoMode==false then — Если всё по серьезному, то ...
res1 = sendTransaction(t) — … передаем сделку по рынку.
end
Наверное, наоборот: демо-режим — это если DemoMode= true?
71 строка у меня так
local aCurrentPrice=tL[0].close — получили текущую цену (ЦПС)
Что нужно править?
Можно ли сделать бота на шорт ещё? Неплохая штука для хэджа портфеля акций через мини индекс.