О чудесах календарного спреда фьючерсов уже доложено в статье некого 3Qu.
smart-lab.ru/blog/586202.php
Поэтому сразу приступим к разоблачению. Какая проделана работа.
В Qukk'е на QLua написан монитор, который с 2020.01.17 по 2020.02.06 каждые 200 мсек записывал в текстовый файл офера и биды RIH0 и RIM0. Эти данные представлены как стандартный файл котировок Метастока, где Open = Bid(H0), High = Ask(H0), Low = Bid(M0), Close = Ask(M0).
Программа WealthLab показывает график этого файла, не понимая его значения. Но мой скрипт на C# по этим данным строит другие графики:
Две точечные линии, зелёные и красные ступеньки, в середине центральной панели:
1) SpreadLong = Ask(H0) — Bid(M0).
2) SpreadShrt = Bid(H0) — Ask(M0).
По цене SpreadShrt приходится продавать спред фьючерсов, когда он дорог а по цене SpreadLong — покупать спред, когда он подешевл.
Чтобы определить, дорог спред или дёшев, строим скользящие средние с горизонтом 10 мин (серые линии)
3) SmaLong.
4) SmaShrt.
Для наглядности проводим сверху и снизу две синие линии, сдвинутые на 30 руб, сконвертированные в пункты фьючерса.
5) UprLong.
6) LwrLong.
Если SpreadLong выше UprLong (на зелёном фоне), считаем фьючерс дорогим и пригодным к продаже.
Если SpreadShrt ниже LwrShrt (на розовом фоне), это случай для покупки.
Коды программ прилагаются.
Так вот, если вкратце, то чудесам мешают три обстоятельства.
Во-первых, средний за день спред котировок RIM0 составляет от 70 до 130 пунктов, т.е. от 0.05% до 0.09%. И это уже перекрывает сколь-нибудь доступный выигрыш от колебаний спреда.
Во-вторых, если кто захочет купить-продать выгоднее, чем в биды-офера, рискует промахнуться и остаться с одним фьючерсом, стоящим против рынка.
И в-третьих. Если спред фьючерсов пойдёт против открытой позиции, ожидание исправления рынка может оказаться слишком долгим и безнадёжным.
Колебания спреда фьючерсов вокруг теоретического равновесного уровня значительны и продолжительны. За 3 отмониторенных недели спред сходил от значения 1636 к 1516 и затем обратно к 1651. Так что говорить о «беспроигрышности» стратегии можно с большой натяжкой.
Значительный выигрыш во многие проценты за день возможен, если совершать сделки посередине между бидами и оферами. Но это доступно только волшебникам. У остальных такая удача будет столь редка, что не оправдает всей затеи.
А если играть по бидам и оферам, то каждый день интрадея будет убыток в несколько десятых процента.
Если кто захочет проверить работу скрипта WealthLab'а, могу почтой послать отмониторенные биды-офера. Они представлены как 1-минутные, чтобы WealthLab мог их сгруппировать от 10- до 60-минуток.
Скрипт-монитор QLua испоьзует также мои библиотечные скрипты SetPaths64.lu, QuikUtil(qu).lua, QuikConst(qc).lua и LuaUtil(lu).lu. Достаточно дееспособный программист сможет заменить их своим кодом.
Вот скрипт C# для WealthLab:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Windows.Forms; // DialogResult
using WealthLab;
using WealthLab.Indicators;
//using mw = MyWealth;
//using mwu = MyWealth.Utils;
namespace WealthLab.Strategies
{
public struct Arg {
public static double MinMoney = 30; // Минимальный выигрыш в рублях
public static double ProfitFactor = 1;// Доля MinMoney для выхода из позиции
public static int PeriodMins = 10; // Период SMA, минуты
public static TimeSpan TimeIni = new TimeSpan (10, 10, 0); // Начало игры
public static TimeSpan TimeBan = new TimeSpan (18, 00, 0); // Запрет входа
public static TimeSpan TimeFin = new TimeSpan (18, 40, 0); // Конец игры
}
public struct Data {
public static double SecPriceStep;// Шаг цены в пунктах фьючерса
public static double DealFee = 4; // Комиссия за сделку в рублях
public static double MinProfit; // Минимальный выигрыш в пунктах
public static int PeriodBars; // Период SMA, бары
}
public class CalendarSpreadTwo : WealthScript
{
DataSeries SpreadLong, SpreadShrt, UprLong, LwrShrt;
DataSeries SpreadMid, UprMid, LwrMid;
protected override void Execute() {
ClearDebug();
if (! Bars.IsIntraday || ! Bars.Scale.Equals (BarScale.Minute)) {
PrintDebug ("Bar.Scale Minute only!");
return;
}
double stepPrice // Рублёвая цена для Data.SecPriceStep
,moneyMargin // Среднее ГО контракта, руб
,moneyFct; // Единица контракта, руб
Prepare (out stepPrice, out moneyMargin, out moneyFct);
//***
Pos pos = new Pos();
double win = 0; double ttlWin = 0;
double minWin = Double.MaxValue; double maxWin = Double.MinValue;
int bar = Data.PeriodBars; TimeSpan curTime = new TimeSpan (0);
for ( ; ; ++bar) {
curTime = new TimeSpan(Date[bar].Hour, Date[bar].Minute, 0);
if (curTime < Arg.TimeIni)
goto Next;
//if (pos.Type == 1 && SpreadShrt[bar] >= UprShrt[bar] ||
// pos.Type == -1 && SpreadLong[bar] <= LwrLong[bar])
if (ExitPos (
bar, false, pos, ref win, ref minWin, ref maxWin, ref ttlWin))
goto Next;
if (curTime >= Arg.TimeBan || pos.Type != 0)
goto Next;
if (SpreadShrt[bar] < LwrShrt[bar]) { // Покупаем
//if (SpreadMid[bar] < LwrMid[bar]) { // Покупаем
EnterPos (1, bar, pos);
} else if (SpreadLong[bar] > UprLong[bar]) { // Продаём
//} else if (SpreadMid[bar] > UprMid[bar]) { // Продаём
EnterPos (-1, bar, pos);
}
Next:
if (curTime >= Arg.TimeFin)
break;
} // for ( ; ; ++ bar
ExitPos (bar, true, pos, ref win, ref minWin, ref maxWin, ref ttlWin);
//***
if (pos.PosNo > 0) {
double margin = 2 * moneyMargin / moneyFct;
double fee = Data.DealFee / moneyFct * pos.PosNo;
double pct = (ttlWin - fee) / margin * 100;
PrintDebug (String.Format("{0,-7};{1,-6};{2,-5};{3,-7};{4,-6};{5,-5}"
,"min", "max", "avr", "ttl", "fee", "pct"));
PrintDebug (String.Format (
"{0,7:F2};{1,6:F2};{2,5:F2};{3,7:F2};{4,6:F2};{5,5:F2}"
,minWin, maxWin, ttlWin / pos.PosNo, ttlWin, fee, pct));
}
} // Execute()
bool EnterPos (int type, int bar, Pos pos) {
pos.Type = type;
pos.EntryBar = bar;
pos.EntryPrice = SpreadPrice (type, bar);
pos.PosNo = pos.PosNo + 1;
return true;
} // EnterPos()
double SpreadPrice (int buySell, int bar) {
if (buySell == 1) {
return SpreadLong[bar];
//double ask1 = High[bar]; double bid2 = Low[bar];
//return ask1 - bid2; // Для покупки спред дорог
} else {
return SpreadShrt[bar];
//double bid1 = Open[bar]; double ask2 = Close[bar];
//return bid1 - ask2; // Для продажи спред дёшев
}
}
bool ExitPos (int bar, bool atOnce, Pos pos, ref double win
,ref double minWin, ref double maxWin, ref double ttlWin) {
if (pos.Type == 0)
return false;
int newType = pos.Type == 1 ? -1 : 1;
double exitPrice = SpreadPrice (newType, bar);
bool mustExit = pos.Type == 1
? exitPrice >= pos.EntryPrice + Data.MinProfit * Arg.ProfitFactor
: exitPrice + Data.MinProfit * Arg.ProfitFactor <= pos.EntryPrice;
if (! mustExit && ! atOnce) return false;
pos.ExitBar = bar;
pos.ExitPrice = exitPrice;
win = 0;
if (pos.Type == 1) // Продаём лонг
win = pos.ExitPrice - pos.EntryPrice;
else // Откупаем шорт
win = pos.EntryPrice - pos.ExitPrice;
minWin = Math.Min (minWin, win);
maxWin = Math.Max (maxWin, win);
ttlWin += win;
if (pos.PosNo == 1)
PrintDebug (
String.Format ("{0,-4};{1,1}:{2,-6};{3,-6};{4,-7};{5,-7};{6,-7}"
,"nn","*", "nBar", "xBar", "nPrc", "xPrc", "win"));
PrintDebug (String.Format (
"{0,4};{1,1};{2,6};{3,6};{4,7:F2};{5,7:F2};{6,7:F2}"
,pos.PosNo, pos.Type == -1 ? "-" : "+", pos.EntryBar, pos.ExitBar
,pos.EntryPrice, pos.ExitPrice, win));
pos.Type = 0;
return true;
} // ExitPos()
void Prepare (out double stepPrice, out double moneyMargin
,out double moneyFct) {
PrintDebug (StrategyName + " " + Bars.Symbol);
Data.SecPriceStep = stepPrice = moneyMargin = moneyFct = 0;
if (Bars.Symbol.Contains ("_OneOfBR")) {
Data.SecPriceStep = 0.01;
stepPrice = 6.14629;
moneyMargin = 4700;
} else if (Bars.Symbol.Contains ("_OneOfGD")) {
Data.SecPriceStep = 0.1;
stepPrice = 6.29685;
moneyMargin = (7756.08+790879)/2;
} else if (Bars.Symbol.Contains ("_OneOfRI")) {
Data.SecPriceStep = 10;
stepPrice = 12.29258;
moneyMargin = 24000;
} else if (Bars.Symbol.Contains ("_OneOfSi") ||
Bars.Symbol.Contains ("_OneOfGZ")) {
Data.SecPriceStep = 1;
stepPrice = 1;
moneyMargin = 4500;
Data.DealFee = 3;
} else if (Bars.Symbol.Contains ("_OneOfSR")) {
Data.SecPriceStep = 1;
stepPrice = 1;
moneyMargin = 4500;
Data.DealFee = 2.50;
} else
return;
moneyFct = stepPrice / Data.SecPriceStep;
Data.MinProfit = Arg.MinMoney / moneyFct;
SetPeriodBars();
SpreadLong = High - Low;
SpreadShrt = Open - Close;
SpreadLong.Description = "SpeadLong";
SpreadShrt.Description = "SpeadShrt";
int m = Bars.Count-1;
PrintDebug ("Spreads " + SpreadLong[m] + " " + SpreadShrt[m]);
PrintDebug ("HighLows " + High[m] + " " + Low[m]);
DataSeries smaLong = SMA.Series (SpreadLong, Data.PeriodBars);
smaLong.Description = "SmaLong";
DataSeries smaShrt = SMA.Series (SpreadShrt, Data.PeriodBars);
smaShrt.Description = "SmaShrt";
UprLong = smaLong + Data.MinProfit;
UprLong.Description = "UprLong";
LwrShrt = smaShrt - Data.MinProfit;
LwrShrt.Description = "LwrShrt";
double avrLong = 0; double avrShrt = 0; double ofrbid = 0;
for (int i = 0; i < Bars.Count; ++i) {
avrLong += SpreadLong[i]; avrShrt += SpreadShrt[i];
ofrbid += Close[i] - Low[i];
}
avrLong /= Bars.Count; avrShrt /= Bars.Count;
ofrbid /= Bars.Count;
int k = -23;
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2} ({2:F0} руб)"
,"Мин.выигрыш", Data.MinProfit, Arg.MinMoney));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Профит-фактор", Arg.ProfitFactor));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}, пункты"
,"Шаг цены фьючерса", Data.SecPriceStep));
PrintDebug (String.Format ("{0,"+(k+3)+"}{1,7}"
,"Период в минутах", Arg.PeriodMins));
PrintDebug (String.Format ("{0,"+(k+3)+"}{1,7}"
,"Период в барах", Data.PeriodBars));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Максимум лонг-спреда", SpreadLong.MaxValue));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Минимум лонг-спреда", SpreadLong.MinValue));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Максимум шорт-спреда", SpreadShrt.MaxValue));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Минимум шорт-спреда", SpreadShrt.MinValue));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Коридор лонг-спреда", SpreadLong.MaxValue - SpreadLong.MinValue));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Коридор шорт-спреда", SpreadShrt.MaxValue - SpreadShrt.MinValue));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Средний лонг-спред", avrLong));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Средний шорт-спред", avrShrt));
PrintDebug (String.Format ("{0,"+k+"}{1,7:F2}"
,"Средний офер-бид", ofrbid));
ChartPane cpSpread = CreatePane (50, true, true);
DrawLabel (cpSpread, "Spread");
PlotSeries (cpSpread, smaLong, Color.Black, LineStyle.Solid, 1);
PlotSeries (cpSpread, smaShrt, Color.Black, LineStyle.Solid, 1);
if (Bars.BarInterval > 1) {
DataSeries diff = smaLong - smaShrt;
ChartPane cpDiff = CreatePane (25, true, true);
PlotSeries (cpDiff, diff, Color.Gray, LineStyle.Histogram, 3);
return;
}
PlotSeries (cpSpread, SpreadLong, Color.Green, LineStyle.Dots, 3);
PlotSeries (cpSpread, SpreadShrt, Color.Red, LineStyle.Dots, 3);
PlotSeries (cpSpread, UprLong, Color.Green, LineStyle.Solid, 1);
PlotSeries (cpSpread, LwrShrt, Color.Red, LineStyle.Solid, 1);
for (int i = 0; i < Data.PeriodBars-1; ++i) {
UprLong[i] = UprLong[Data.PeriodBars-1];
LwrShrt[i] = LwrShrt[Data.PeriodBars-1];
SetSeriesBarColor (i, UprLong, Color.Empty);
SetSeriesBarColor (i, LwrShrt, Color.Empty);
}
int upr = 0; int lwr = 0; int uprlwr = 0;
for (int i = Data.PeriodBars-1; i < Bars.Count; ++i) {
if (SpreadLong[i] >= UprLong[i] && SpreadShrt[i] > LwrShrt[i]) {
SetPaneBackgroundColor (cpSpread, i, Color.LightGreen);
++upr;
} else if (SpreadShrt[i] <= LwrShrt[i] && SpreadLong[i] < UprLong[i]) {
SetPaneBackgroundColor (cpSpread, i, Color.Pink);
++lwr;
} else if (SpreadLong[i] >= UprLong[i] && SpreadShrt[i]<= LwrShrt[i]) {
SetPaneBackgroundColor (cpSpread, i, Color.LightYellow);
++uprlwr;
}
}
int j = Bars.Count-10;
AnnotateChart(cpSpread, SpreadLong[j].ToString()
,j, SpreadLong[j]+17, Color.Black);
AnnotateChart(cpSpread, SpreadShrt[j].ToString()
,j, SpreadShrt[j]-7, Color.Red);
k = -20;
PrintDebug (String.Format ("{0,"+k+"}{1,7} ({2,5:F2}%)"
,"Пробоев вверх", upr
,100.0 * upr / (Bars.Count - Data.PeriodBars)));
PrintDebug (String.Format ("{0,"+k+"}{1,7} ({2,5:F2}%)"
,"Пробоев вниз", lwr
,100.0 * lwr / (Bars.Count - Data.PeriodBars)));
PrintDebug (String.Format ("{0,"+k+"}{1,7} ({2,5:F2}%)"
,"Пробоев вверх и вниз", uprlwr
,100.0 * uprlwr / (Bars.Count - Data.PeriodBars)));
SpreadMid = ((High + Open) - (Close + Low)) / 2;
SpreadMid.Description = "SpreadMid";
DataSeries smaMid = SMA.Series (SpreadMid, Data.PeriodBars);
smaMid.Description = "SmaMid";
ChartPane cpMid = CreatePane (25, true, true);
DrawLabel (cpMid, "Mid");
PlotSeries (cpMid, SpreadMid, Color.Black, LineStyle.Solid, 1);
PlotSeries (cpMid, smaMid, Color.Blue, LineStyle.Solid, 1);
UprMid = smaMid + Data.MinProfit;
UprMid.Description = "UptMid";
LwrMid = smaMid - Data.MinProfit;
LwrMid.Description = "LwrMid";
PlotSeries (cpMid, UprMid, Color.Green, LineStyle.Solid, 1);
PlotSeries (cpMid, LwrMid, Color.Red, LineStyle.Solid, 1);
for (int i = 0; i < Data.PeriodBars-1; ++i) {
UprMid[i] = UprMid[Data.PeriodBars-1];
LwrMid[i] = LwrMid[Data.PeriodBars-1];
SetSeriesBarColor (i, UprMid, Color.Empty);
SetSeriesBarColor (i, LwrMid, Color.Empty);
}
for (int i = Data.PeriodBars-1; i < Bars.Count; ++i) {
if (SpreadMid[i] > UprMid[i])
SetPaneBackgroundColor (cpMid, i, Color.LightGreen);
else if (SpreadMid[i] < LwrMid[i])
SetPaneBackgroundColor (cpMid, i, Color.Pink);
}
} // Prepare()
void SetPeriodBars() {
if (Bars.BarInterval > 1) { // Arg.PeriodMins) {
Arg.PeriodMins = Bars.BarInterval;
Data.PeriodBars = 1;
return;
}
int barsPerMin = 60 * Int32.Parse (
Bars.Symbol [Bars.Symbol.Length-1].ToString());
Data.PeriodBars = Arg.PeriodMins * barsPerMin / Bars.BarInterval;
}
} // class CalendarSpreadTwo
class Pos {
public int Type = 0; // 1 - long, -1 - short
public int EntryBar, ExitBar;
public double EntryPrice, ExitPrice;
public int PosNo = 0;
}
} // namespace WealthLab.Strategies
Это головной скрипт монитора на QLua для Quik:
-- Мониторим котировки ближнего и дальнего фьючерсов по тикам
Data = { "RIH0", "RIM0" -- дорогой и дешёвый фьючерсы
,MsDlt = 200 -- период опроса очереди заявок, мсек
}
function OnInit (scriptPath)
dofile ("D:\\BAT\\Lua\\SetPaths64.lua")
Require { qu = "QuikUtil(qu)" } -- lu, qc, tu, wau
ScriptDir, ScriptName = lu.SplitPath (scriptPath)
end -- OnInit()
function OnStop (signal) -- 1 по кнопке Остановить
StopFlag = true -- 2 при закрытии Quik'а
return 3000 -- Вместо стандартных 5 сек
end -- OnStop()
function main()
dofile (ScriptDir .."OneOf_Lib.lua")
local frame = MakeFrame() -- Неуклюжая идея. Исправить!!!
message (ScriptName ..": Start")
while not StopFlag and frame.HandleTick() do end
message (ScriptName ..": Quit")
end -- main()
А это его вспомогательный dofile:
-- dofile монитора котировок ближнего и дальнего фьючерсов по тикам
-- Константы
local Header =
"<TICKER>;<PER>;<DATE>;<TIME>;<OPEN>;<HIGH>;<LOW>;<CLOSE>;<VOL>"
local per = "1"
local tmBeg, tmEnd = "10000", "184500"
--local tmBeg, tmEnd = "00000", "235000"
-- Параметры: Data.MsDlt
-- Data[1],[2] -- дорогой и дешёвый фьючерсы, "BRG0", "BRH0" и т.п.
local ticker = Data[1] .."-".. Data[2]
-- Общие: Log, ScriptDir С конечным "\", ScriptName Без расширения
local ymd = os.date ("%Y%m%d", os.time())
local sfx = Data.MsDlt == 200 and 5 or
Data.MsDlt == 500 and 2 or nil
if not sfx then error ("Invalid MsDlt", 2) end
Log = io.open (ScriptDir .."Log\\".. ymd .."_".. ScriptName
.. sfx ..".csv", "w")
Log:write (Header)
function MakeFrame ()
local ask1, bid1, ask2, bid2, sprd
local frm = Data[1]:sub (1, 2) == "GD" and
"%s;%s;%s;%s;%.1f;%.1f;%.1f;%.1f;%.2f"
or Data[1]:sub (1, 2) == "BR" and
"%s;%s;%s;%s;%.2f;%.2f;%.2f;%.2f;%.3f"
or "%s;%s;%s;%s;%d;%d;%d;%d;%d" -- GZ, RI, Si, SR
local pre, no = os.time(), 0 -- Ловим начало секунды
local cur = pre
while cur == pre do sleep (1); cur = os.time() end
pre = cur
local m = {}
m.HandleTick = function()
local tm = os.date ("%H%M%S", cur)
if tm >= tmEnd then return false end
if tm >= tmBeg then
no = no + 1
ask1 = qu.GetParamNum (qc.SPBFUT, Data[1], qc.OFFER)
ask2 = qu.GetParamNum (qc.SPBFUT, Data[2], qc.OFFER)
bid1 = qu.GetParamNum (qc.SPBFUT, Data[1], qc.BID)
bid2 = qu.GetParamNum (qc.SPBFUT, Data[2], qc.BID)
sprd = (ask1 + bid1) / 2 - (ask2 + bid2) / 2
local msg = string.format (frm, ticker, per
,os.date ("%Y%m%d", cur), tm
,bid1, ask1, bid2, ask2, no)
Log:write ("\n".. msg)
end -- if tm >= tmBeg
sleep (Data.MsDlt)
cur = os.time()
if cur > pre then
Log:flush()
pre, no = cur, 0
end
return true
end -- m.HandleTick()
return m
end -- MakeFrame()
У Вас есть положительный опыт торговли этого календарника? Или это тоже только в теории на минутках?
В опционах ликвидность похуже, но ведь торгуют и не жалуются.
upd. Хотя прошу прощение. Если ставка по маркету плавает, то между фучами да, ставка тоже поплывет
Боюсь, мне это недоступно. Я играю через домашний ПК.
Оосновная проблема в том, что внутридневный размах движения спреда фьючерса (и в самые мелкие мксек, и за часы) сопоставим со спредом бидов-оферов дальнего фьючерса.
К тому же, чтобы не застревать надолго в позиции, надо угадывать направление движения спреда фьчерсов, что мне тоже не дано.
Характерная ситуация. В конце дня позиция застряла в убытке. Что делать? Переносить на следующий день и получить углубление убытка или рубить позицию с потерей дневной прибыли?
привожу график по RTS-3.20 за 1 секунду, за которую было огромное количество возможностей, в том числе и календарного арбитража с RTS-6.20.
По RTS-3.20 за эту секунду проторговано более 5 тысяч лотов.
По RTS-6.20 за этот же период проторговано 11 (одиннадцать!) лотов.
Где все арбитражеры то?
это не просто секунда. Это уникальная секунда. Если вы этого не видите на картинке, мне жаль.