Рабочая внутридневная трендовая стратегия, с кодом автоматизации теста для Multicharts и кодом для «боевой» торговли через MetaTrader 5. Все описанное разработанно и проверено на фьючерсе на индекс РТС (ФОРТС). Я не претендую ни на какие звания и не принимаю никаких претензий или критики по поводу использования данного материала.
Небольшое лирическое отступление.
Почему пишу публично, если «такая корова самому нужна»? На то есть ряд причин:
Помимо вышесказанного, я никому ничего не продаю, не консультирую, не управляю никакими капиталами и не являюсь публичной персоной, так что никакого подвоха. Мой личный интерес лежит в области реального сектора, где я заработал денег много больше, чем на этих ваших биржах.
Не буду рассказывать о доходностях, которые можно заработать с помощью этой стратегии, каждый сам подбирает оптимальные параметры соотношения риска к прибыльности. С идеей и кодом читателю прийдется разбираться, делать тесты, анализировать, тратить силы и время, это не волшебная таблетка от бедности, но вполне рабочая идея.
Тестирование лучше проводить в Multicharts, так как в MetaTrader 5 есть особенности исполнения по ценам bid/ask/lastprice, что иногда искажает реальную картину (если бы велась торговля по биржевым ценам).
Торговля на MT5 велась через брокера «Открытие», скорость исполнения приказов более чем удовлетворительная. Это не реклама и не отсылка, а только голый факт.
Программист из меня так себе, код программ не слишком красивый и не оптимизирован, зато работает.
Общий алгоритм работы.
Существенные замечания.
Параметр стоп-лосс подобран империческим путем. Для каждого инструмента и в зависимости от текущей волатильности он должен быть скорректирован. В действительности, для фьючерса на индекс РТС этот параметр остается неизменным, а вот для фьючерса на валютную пару доллар-рубль он изменился из-за сильного изменения стоимости контракта.
Бэк-тесты показали сильную зависимость доходность системы сильно влияет время открытия LSE. Не нужно пытаться это объяснить, просто принять как факт.
Как избежать «распила». В основе всей идеи лежит факт того, что рыночные процессы цикличны. Периоды застоя сменяют тренды и наоборот. Для того, чтобы «не лезть в рынок» в неподходящее время, нужно дождатся расчетного (виртуального) убытка на счете.
Статистически верхняя граница убыточной зоны приблизительно составляет десять тысяч пунктов, нижняя составляет двадцать пять тысяч пунктов на каждый задействованный контракт. Для того, чтобы высчитать просадку, нужно суммировать все полученные прибыли и убытки на базовом количестве контрактов (три).
На развитых рынках с хорошей ликвидностью трендовые периоды очень тяжело выявлять, там нет таких движений цены внутри дня, как на наших Ri и Si, так что там прийдется менять параметры и глубоко тестировать.
Пример.
В первый день торгов произошел вход в позицию, первая часть была закрыта 1 х 500 пунктов, вторая часть (трейлинг стоп) была закрыта по 1 х 800, и третья часть была закрыта 1 х 1500. Итого: 500 + 800 + 1500 = 2800 пунктов. Во второй день произошел вход в позицию и после этого вся позиция была закрыта по стоп-лоссу 3 х (500) пунктов. После этого произошел еще один вход в позицию, первая часть закрылась по 1 х 500 пунктов, оставшиеся две трети были закрыты по стоп-лоссу 2 х (500) пунктов. Итого: (1500) + 500 + (1000) = (2000) пунктов.
После этого сальдируем накопленные прибыли и убытки и получаем: 2800 + (2000) = 800 пунктов. Это и есть необходимое расчетное текущее значение просадки.
Статистически выход из тренда лучше осуществлять при выходе из просадки в плюсовую зону, либо при прибыли на счете около 20% от предыдущей суммы депозита.
Исходный код PowerLanguage для тестирования на исторических данных:
[IntrabarOrderGeneration = True]
inputs: StopLoss(500), LotSize(3), StartTime(1100), FinishTime(2300), CloseTime(2345);
vars: TargetPrice(0), TargetLong(0), TargetShort(0), TargetLongTake(0), TargetShortTake(0), trailing(0);setstopcontract;
SetStopLoss(StopLoss);if time = StartTime then
begin
TargetPrice = Open;TargetLong = TargetPrice + StopLoss;
TargetShort = TargetPrice — StopLoss;
TargetLongTake = TargetLong + StopLoss;
TargetShortTake = TargetShort — StopLoss;
end;
if marketposition = 0 and time >= StartTime and time <= FinishTime Then
begin
trailing = TargetPrice;
buy («TargetLong») LotSize contracts next bar at TargetLong stop;
sell short («TargetShort») LotSize contracts next bar at TargetShort stop;
end;if marketposition > 0 and time >= StartTime and time <= FinishTime Then
begin
sell («PartialCoverLong») (LotSize / 3) contracts next bar at TargetLongTake limit;if trailing < High — StopLoss * 3 then
begin
trailing = High — StopLoss * 3;
text_new(D, Time, High, text(trailing));
end;
sell («TrailingStopLong») (LotSize / 3) contracts next bar at trailing stop;
end;if marketposition < 0 and time >= StartTime and time <= FinishTime Then
begin
buy to cover («PartialCoverShort») (LotSize / 3) contracts next bar at TargetShortTake limit;
if trailing > Low + StopLoss * 3 then
begin
trailing = Low + StopLoss * 3;
text_new(D, Time, Low, text(trailing));
end;
buy to cover («TrailingStopShort») (LotSize / 3) contracts next bar at trailing stop;
end;if time = CloseTime then
begin
if marketposition < 0 then
begin
buytocover («EOD short») next bar market;
end;
if marketposition > 0 then
begin
sell («EOD long») next bar market;
end;
end;
Исходный код MQL5 для MetaTrader 5:
#include <Trade/Trade.mqh>
#include <Trade/SymbolInfo.mqh>#property version «5.0»
//--- input variables
input double StopLoss = 500; // Initial stop-loss
input double lotSize = 3; // Initial lot size
input double lotSizeStep = 3; // Initial lot size increment
input double TargetPriceOverride = 0; // Override TargetPriceinput int startHour = 11; // Start time
input int endHour = 22; // Finish time
input int EODH = 23; // End Of Day Hour
input int EODM = 40; // End Of Day Minute// Debugging
input double const InitialBalance = 500000;
input bool const DEBUG_ENABLE = false;//--- variables
MqlDateTime CurrentTime;
MqlTick Tick;
CTrade Trade;
CPositionInfo Position;
CSymbolInfo Sym;
COrderInfo Order;double TargetPrice, TargetLong, TargetShort, Lots, Balance = 100, MaxBalance = 0, Xbalance = 0, Xmin = 100, Xinit = 0, Y = 0, TrailingStop = 0;
ulong TrailingTicket = -1;
string const TrailingComment = «trailing», InfoLabelName = «InfoLabel»;void OnTick() {
SymbolInfoTick(Symbol(), Tick);
TimeCurrent(CurrentTime);if(CurrentTime.hour >= startHour && CurrentTime.hour < endHour) {
CalculatePrice();
if(OrdersTotal() < 4 && PositionsTotal() < 1) {
if((Tick.last < TargetLong — StopLoss / 1.5) && (Tick.last > TargetShort + StopLoss / 1.5)) {
DEBUG(«CurrentTime to refill orders!»);
CleanUpOrders();
SetupOrders();
}
}
}if(CurrentTime.hour == endHour) {
// possible caveat
if(OrdersTotal() > 1) {
DEBUG(«CurrentTime to wipe out orders»);
CleanUpOrders();
}
}if(CurrentTime.hour == EODH && (CurrentTime.min == EODM || CurrentTime.min == EODM + 1 || CurrentTime.min == EODM + 2 || CurrentTime.min == EODM + 3 || CurrentTime.min == EODM + 4)) {
if(PositionsTotal() > 0) {
DEBUG(«CurrentTime to close up positions»);
CleanUpOrders(false);
CleanUpPositions();
}
}// trailing stop
for(int i = 0; i < PositionsTotal(); i++) {
if(Symbol() == PositionGetSymbol(i)) {
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
if (Tick.last — StopLoss * 2 > TrailingStop) {
TrailingStop = Tick.last — StopLoss * 2;if (TrailingTicket == -1) {
DEBUG(«setting up trailing SellStop = » + (string)TrailingStop);bool tres = Trade.SellStop(volume() / 3, TrailingStop, _Symbol, 0, 0, ORDER_TIME_DAY, 0, TrailingComment);
if (tres) {
TrailingTicket = Trade.ResultOrder();
DEBUG(«trailing set up, ticket = » + (string)TrailingTicket);
}
} else {
if (Order.Select(TrailingTicket)) {
DEBUG(«moving up trailing SellStop = » + (string)TrailingStop);
Trade.OrderModify(TrailingTicket, TrailingStop, 0, 0, ORDER_TIME_DAY, 0, 0);
}
}
}
}if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) {
if (Tick.last + StopLoss * 2 < TrailingStop) {
TrailingStop = Tick.last + StopLoss * 2;if (TrailingTicket == -1) {
DEBUG(«setting up trailing BuyStop = » + (string)TrailingStop);bool tres = Trade.BuyStop(volume() / 3, TrailingStop, _Symbol, 0, 0, ORDER_TIME_DAY, 0, TrailingComment);
if (tres) {
TrailingTicket = Trade.ResultOrder();
DEBUG(«trailing set up, ticket = » + (string)TrailingTicket);
}
} else {
if (Order.Select(TrailingTicket)) {
DEBUG(«moving down trailing BuyStop = » + (string)TrailingStop);
Trade.OrderModify(TrailingTicket, TrailingStop, 0, 0, ORDER_TIME_DAY, 0, 0);
} else {
DEBUG(«unable to select TrailingTicket: » + (string)TrailingTicket);
}
}
}
}
}
}DisplayStats();
}void CalculatePrice() {
double price[];
MqlDateTime OpenTime;TimeCurrent(OpenTime);
OpenTime.hour = startHour;
OpenTime.min = 00;
OpenTime.sec = 00;CopyOpen(_Symbol, PERIOD_CURRENT, StructToTime(OpenTime), 1, price);
TargetPrice = price[0];
TargetLong = TargetPrice + StopLoss;
TargetShort = TargetPrice — StopLoss;
}void SetupOrders() {
TrailingTicket = -1;
TrailingStop = TargetPrice;Trade.SetTypeFilling(ORDER_FILLING_RETURN);
Trade.BuyStop (volume(), TargetLong, _Symbol, TargetLong — StopLoss, 0, ORDER_TIME_DAY);
Trade.SellStop(volume(), TargetShort, _Symbol, TargetShort + StopLoss, 0, ORDER_TIME_DAY);// First out
Trade.SellLimit(volume() / 3, TargetLong + StopLoss, _Symbol, 0, 0, ORDER_TIME_DAY);
Trade.BuyLimit (volume() / 3, TargetShort — StopLoss, _Symbol, 0, 0, ORDER_TIME_DAY);DisplayStats();
}void OnChartEvent(const int id, // идентификатор события
const long& lparam, // параметр события типа long
const double& dparam, // параметр события типа double
const string& sparam // параметр события типа string
) {if (id == CHARTEVENT_KEYDOWN) {
//DEBUG(«key pressed = » + lparam);
switch (lparam)
{
case 116: // F5
if (TargetPriceOverride > 0) {
TargetPrice = TargetPriceOverride;
DEBUG(«manual recalculate prices!»);
}
CalculatePrice();
TrailingStop = TargetPrice;
DisplayStats();
break;
case 220: // \
DEBUG(«manual setup orders!»);
SetupOrders();
break;
}
}
}void DisplayStats() {
CalculateBalance();
string info = StringFormat(
"\n InitialBalance = %0.2f\n CurrentBalance = %0.2f\n MaxBalance = %0.2f" +
"\n =======================================" +
"\n InitialBalanceFactor = %0.2f\n CurrentBalanceFactor = %0.2f\n MinBalanceFactor = %0.2f" +
"\n =======================================" +
"\n InitialMargin = %0.2f\n CurrentLotSize = %0.2f" +
"\n =======================================" +
"\n TargetPrice = %0.2f\n TargetLong = %0.2f\n TargetShort = %0.2f\n TrailingTicket = %d\n TrailingStop = %0.2f\n",
InitialBalance, Balance, MaxBalance,
Y, Xbalance, Xmin,
Sym.MarginInitial(), volume(),
TargetPrice, TargetLong, TargetShort, TrailingTicket, TrailingStop);
Comment(info);
}void CleanUpPositions() {
//--- close up all positions
DEBUG(«PositionsTotal = » + (string)PositionsTotal());
Trade.PositionClose(_Symbol);
}//--- clean up all orders
void CleanUpOrders(bool keepTrailing = true) {
ulong ticket = 0;DEBUG(«OrdersTotal = » + (string)OrdersTotal());
for(int pos = OrdersTotal(); pos >= 0; pos--) {
ticket = OrderGetTicket(pos);
if(Order.Select(ticket)) {
DEBUG(«got ticket = » + (string)ticket + " by index = " + (string)pos + " | comment = " + Order.Comment());
}if(Order.Symbol() == _Symbol) {
if (Order.Comment() == TrailingComment && keepTrailing) {
DEBUG(«this is trailing stop, requested to keep-alive»);
continue;
}DEBUG(«going to delete ticket = » + (string)ticket);
Trade.OrderDelete(ticket);
}
}
}//+------------------------------------------------------------------+
//| volume |
//+------------------------------------------------------------------+
double volume() {
CalculateBalance();if (Xbalance > 90 && Xbalance <= 95) {
return lotSize + lotSizeStep;
}if (Xbalance > 85 && Xbalance <= 90)
return lotSize + lotSizeStep * 2;if (Xbalance > 80 && Xbalance <= 85)
return lotSize + lotSizeStep * 3;if (Xbalance <= 80)
return lotSize + lotSizeStep * 4;return lotSize;
}// updates new balance high
void CalculateBalance() {
Balance = AccountInfoDouble(ACCOUNT_BALANCE);if(MaxBalance < Balance)
MaxBalance = Balance;Xbalance = Balance * 100 / MaxBalance;
Y = Balance * 100 / InitialBalance;if(Xbalance < Xmin)
Xmin = Xbalance;
}void DEBUG(string message) {
if (!DEBUG_ENABLE)
return;Print(" — DEBUG: ", message);
}//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit() {
Trade.LogLevel(LOG_LEVEL_NO);Sym.Name(_Symbol);
CalculateBalance();
DisplayStats();return(0);
}