Хочу поделиться полезным дополнением к WealthLab’у. Около года назад уже выкладывал черновик к нему на одном трейдерском ресурсе, но там алгоритмистов и системщиков было мало, поэтому код там мало кому оказался полезным.
Тестируя различные стратегии на длительном периоде, всегда приходилось в уме делать примерную корректировку результатов, выдаваемых WealthLab’ом. Моя стандартная схема проверки – один контракт без реинвестирования. Однако WealthLab все результаты выдает в пунктах, что несколько искажает информацию, т.к. прибыль или убыток в 1000п на контракт в январе 2009го года, когда fRTS был 40000п, и в августе 2011, когда fRTS был 200000п, это небо и земля. На мой взгляд, гораздо более информативно отображать результат каждой сделки в процентах от текущей цены.
В самом деле: имея 1млн рублей, в январе 2009-го можно было купить около 40 контрактов (это на глаз) без плеча, а в августе 2011-го на ту же сумму можно было приобрести только 8 контрактов. И движение цены в 1000п в первом случае увеличило бы счет на 24тыс руб, в то время как во втором – только на 5тыс руб. При этом WealthLab не отразит этой разницы, записав и в том и в другом случае +1000п. Это же касается и просадок: выданный WealthLab’ом результат DrawDown = 8000п не совсем информативен, т.к. в реальности для разных периодов истории это показание может различаться в 5 раз. Поэтому я и посчитал, что значительно удобнее было бы выводить результаты прохода WealthLab’ом не в пунктах, а в процентах.
В дополнение можно сказать, что WealthLab, в общем-то, дает довольно скудный набор данных для оценки результатов. Скажем, любимая всеми Maximum Drowdawn (и как следствие – Recovery factor) показывает только пиковое значение. Поэтому стратегия с 10ю просадками по 10000п и с одной в 10000п, но девятью по 1000п будут выглядеть глазами WealthLab’а одинаково. Само собой, можно заглядывать в эквити, в график просадок и график обновления максимумов, но это можно делать на единичной выборке параметров, а получить общую картину при оптимизации не получится.
В общем, WealthLab немного не устраивает предоставляемыми возможностями анализа, особенно в режиме оптимизации. Поэтому был написан небольшой класс SData, который очень просто подключается к скрипту WealthLab’а. Каждый раз после одного прохода стратегии этот класс выполняет запись в файл таблицы результатов (Performance), но не в пунктах, а в процентах. Кроме того, этот класс позволяет вводить любые дополнительные расчеты (для примера у меня приведен код оценки прямолинейности получаемой эквити). А в режиме оптимизации – выкидывать результаты, заведомо не интересные для оценки (например, с Win% < 30%). Полученный файл легко импортируется для анализа в Excel, где можно уже строить графики, выполнять сортировку и т.д. Считаю, что данный класс может оказаться полезным пользователям WealthLab’а.
Код самого класса:
- Код: Выделить всё
struct SData
{
public string Path;
public StrategyParameter []SP;
public string[] SubNames;
double Min_NetProfit;
double Min_Average;
double Min_MaxDrawdown;
int Min_Trades;
double Min_Win;
double Min_ProfitFactor;
double Min_RecoveryFactor;
double Min_Linearity;
double Min_Product;
double Min_ProductDrawdown;
double Max_NetProfit;
double Max_Average;
double Max_MaxDrawdown;
int Max_Trades;
double Max_Win;
double Max_ProfitFactor;
double Max_RecoveryFactor;
double Max_Linearity;
double Max_Product;
double Max_ProductDrawdown;
//--------------------------------------------------------------
//
// Функции установки правил протоколирования.
//
// При выполнении оптимизации WealthLab'ом в файл протокола
// (тот, что указывается в SData.create_file) будут заноситься
// только характеристики только тех проходов, которые удовлетворяют
// устновленным условиям
//
//--------------------------------------------------------------
public void set_limit_NetProfit (double min, double max) { Min_NetProfit = min; Max_NetProfit = max; }
public void set_limit_Average (double min, double max) { Min_Average = min; Max_Average = max; }
public void set_limit_MaxDrawdown (double min, double max) { Min_MaxDrawdown = min; Max_MaxDrawdown = max; }
public void set_limit_Trades (int min, int max) { Min_Trades = min; Max_Trades = max; }
public void set_limit_Win (double min, double max) { Min_Win = min; Max_Win = max; }
public void set_limit_ProfitFactor (double min, double max) { Min_ProfitFactor = min; Max_ProfitFactor = max; }
public void set_limit_RecoveryFactor (double min, double max) { Min_RecoveryFactor = min; Max_RecoveryFactor = max; }
public void set_limit_Linearity (double min, double max) { Min_Linearity = min; Max_Linearity = max; }
public void set_limit_Product (double min, double max) { Min_Product = min; Max_Product = max; }
public void set_limit_ProductDrawdown (double min, double max){ Min_ProductDrawdown = min; Max_ProductDrawdown = max; }
//--------------------------------------------------------------
//
// Данная функция должна вызываться в конструкторе перед вызовом
// create_file. Здесь мы указываем, какие параметры (StrategyParameter)
// протоколировать
//
//--------------------------------------------------------------
public bool init_params(params StrategyParameter[] strategy_parameters)
{
SP = strategy_parameters;
// Инициалиируем лимиты пустыми значениями
// Если min = mix, то параметр не проверяется на лимиты
Min_NetProfit = 0;
Min_Average = 0;
Min_MaxDrawdown = 0;
Min_Trades = 0;
Min_Win = 0;
Min_ProfitFactor = 0;
Min_RecoveryFactor = 0;
Min_Linearity = 0;
Min_Product = 0;
Min_ProductDrawdown = 0;
Max_NetProfit = 0;
Max_Average = 0;
Max_MaxDrawdown = 0;
Max_Trades = 0;
Max_Win = 0;
Max_ProfitFactor = 0;
Max_RecoveryFactor = 0;
Max_Linearity = 0;
Max_Product = 0;
Max_ProductDrawdown = 0;
return true;
}
//--------------------------------------------------------------
//
// Функция вызывается из конструктора и создает несколько файлов.
// В каждый файл будут записываться раздельно результаты работы
// стратегии для конкретного типа сигналов. Разделение идет поименное,
// причем имя указывается в аргументах WL'овских функций, например:
// ShortAtMarket(bar + 1, "SELL");
// LongAtMarket(bar + 1, "BUY");
// - и т.д.
//
// Используемые имена перечисляются в аргументах функции create_file:
//
// create_file(@"C:\my_data\", "BUY", "SELL", "");
//
// Указание в списке аргументов пустой строки создаст еще общий файл для
// всех сигналов
//
//--------------------------------------------------------------
public bool create_file(string path, params string[] subnames)
{
System.IO.StreamWriter f;
Path = path;
SubNames = subnames;
if (SubNames == null)
{
SubNames = new string[1];
SubNames[0] = "";
}
foreach (string subname in SubNames)
{
string fname = Path + "opti_" + subname + ".txt";
try {f = new System.IO.StreamWriter(fname, false);}
catch {System.Windows.Forms.MessageBox.Show("Ошибка SData:\n\nНе удается открыть файл\n" + fname +".\n\nПроверьте путь."); return false;}
if (f == null) return false;
foreach (StrategyParameter sp in SP) if (sp != null) f.Write(sp.Name + "\t");
f.Write( "NetProfit"); // Сумма процентных приращений
f.Write("\t" + "Trades"); // Количество сделок
f.Write("\t" + "Wins"); // Количество прибыльных сделок
f.Write("\t" + "Losses"); // Количество убыточных сделок
f.Write("\t" + "Average"); // Средняя прибыль на сделку (%)
f.Write("\t" + "Win%"); // Процент прибыльных
f.Write("\t" + "MaxDrawdown"); // Максимальная просадка (по процентным приращениям)
f.Write("\t" + "GrossProfit"); // Сумма всех прибыльных сделок
f.Write("\t" + "GrossLoss"); // Сумма всех убыточных сделок
f.Write("\t" + "ProfitFactor"); // Отношение прибыль/убыток (по процентным приращениям)
f.Write("\t" + "RecoveryFactor"); // Отношение прибыль/макс.просадка (по процентным приращениям)
f.Write("\t" + "Linearity"); // Линейность эквити
f.Write("\t" + "Product"); // Произведение процентных приращений (для оценки реинвестирования)
f.Write("\t" + "ProductDrawdown"); // Просадка при реинвестировании (в % от счета)
f.WriteLine("");
f.Close();
}
return true;
}
//--------------------------------------------------------------
// write_file
//
// Функция вызывается после выполнения кода стратегии (чаще сразу
// за основным циклом по барам), выполняет все расчеты по
// проделанным сделкам и заполняет файлы результирующими данными.
//
//--------------------------------------------------------------
public void write_file(IList<Position> positions)
{
foreach (string subname in SubNames)
{
//----------------------------------------------------
// Рабочие переменные
//----------------------------------------------------
double GrossProfit = 0.0; // Общая прибыль
double GrossLoss = 0.0; // Общий убыток
double NetProfit = 0; // Весь профит по процентным приращениям
double MaxDrawdown = 0.0; // Максимальная просадка по процентным приращениям
double Integral = 0; // Площадь просадки
double Linearity = 0;
int Trades = 0; // Сделок всего
int Wins = 0; // Прибыльных
int Losses = 0; // Убыточных
double Product = 1; // Изменение счета с полным реинвестированием
double ProductDrawdown = 1.0; // Просадка счета при полном реинвестировании (%)
//----------------------------------------------------
// Не обрабатываем, если не было сделок
//----------------------------------------------------
if (positions == null || positions.Count == 0) return;
//----------------------------------------------------
// Вспомогательные переменные
//----------------------------------------------------
double MaxProductDrawdown = 1.0;
double MaxProfit = 0;
double MaxProduct = 1;
int n;
//----------------------------------------------------
// Вычисление результатов стратегии
//----------------------------------------------------
int LastBar = positions[0].EntryBar;
if (positions != null) foreach (Position p in positions)
{
int bars = p.EntryBar - LastBar;
if (subname != "" && p.EntrySignal != subname) continue;
LastBar = p.ExitBar;
NetProfit += p.NetProfitPercent;
Trades++;
if (p.NetProfit > 0)
{
Wins++;
GrossProfit += p.NetProfitPercent;
}
else
{
Losses++;
GrossLoss += p.NetProfitPercent;
}
MaxProfit = Math.Max(NetProfit, MaxProfit);
MaxDrawdown = Math.Min(NetProfit - MaxProfit, MaxDrawdown);
Product *= (1.0 + p.NetProfitPercent / 100.0);
MaxProduct = Math.Max(Product, MaxProduct);
ProductDrawdown = Math.Min(Product / MaxProduct, ProductDrawdown);
}
//----------------------------------------------------
// Оценка отклонения эквити от идеальной
//----------------------------------------------------
Position FirstPos = positions[0];
Position LastPos = positions[positions.Count-1];
int TotalBars = LastPos.EntryBar + LastPos.BarsHeld; // Вся шкала эквити по x
double Equity = 0.0;
double k = NetProfit / TotalBars; // Коэффициент наклона линии идеальной эквити
double y = 0.0, dy;
int x;
double maxdy = 0.0, mindy = 0.0;// максимальные отклонения от прямой вверх и вниз
if (positions != null) foreach (Position p in positions)
{
if (subname != "" && p.EntrySignal != subname) continue;
x = p.ExitBar;
double ideal_y = k*x;
y += p.NetProfitPercent;
dy = ideal_y - y;
mindy = Math.Min(dy, mindy);
maxdy = Math.Max(dy, maxdy);
}
Linearity = NetProfit==0? 10E10 : Math.Abs((maxdy - mindy) / NetProfit);
//----------------------------------------------------
// Добавляем в файл результат прохода стратегии
//----------------------------------------------------
double ProfitFactor = GrossLoss==0? 10E10 : -GrossProfit/GrossLoss;
double RecoveryFactor = MaxDrawdown==0? 10E10 : Math.Max(-NetProfit/MaxDrawdown, 0);
double Win = (double)Wins/Trades*100.0;
double Average = NetProfit/Trades;
Product = (Product - 1) * 100;
ProductDrawdown = (1 - ProductDrawdown) * 100;
//----------------------------------------------------
// Отсекаем ненравящиеся нам результаты
//----------------------------------------------------
if (Min_NetProfit != Max_NetProfit && (Min_NetProfit > NetProfit || Max_NetProfit < NetProfit)) return;
if (Min_Average != Max_Average && (Min_Average > Average || Max_Average < Average)) return;
if (Min_MaxDrawdown != Max_MaxDrawdown && (Min_MaxDrawdown > MaxDrawdown || Max_MaxDrawdown < MaxDrawdown)) return;
if (Min_Trades != Max_Trades && (Min_Trades > Trades || Max_Trades < Trades)) return;
if (Min_Win != Max_Win && (Min_Win > Win || Max_Win < Win)) return;
if (Min_ProfitFactor != Max_ProfitFactor && (Min_ProfitFactor > ProfitFactor || Max_ProfitFactor < ProfitFactor)) return;
if (Min_RecoveryFactor != Max_RecoveryFactor && (Min_RecoveryFactor > RecoveryFactor || Max_RecoveryFactor < RecoveryFactor)) return;
if (Min_Linearity != Max_Linearity && (Min_Linearity > Linearity || Max_Linearity < Linearity)) return;
if (Min_Product != Max_Product && (Min_Product > Product || Max_Product < Product)) return;
if (Min_ProductDrawdown != Max_ProductDrawdown && (Min_ProductDrawdown> ProductDrawdown || Max_ProductDrawdown < ProductDrawdown)) return;
System.IO.StreamWriter f;
string fname = Path + "opti_" + subname + ".txt";
try {f = new System.IO.StreamWriter(fname, true);}
catch {System.Windows.Forms.MessageBox.Show("Ошибка SData:\n\nНе удается открыть файл\n" + fname +".\n\nПроверьте путь."); return;}
foreach (StrategyParameter sp in SP) if (sp != null) f.Write(sp.Value + "\t");
f.Write( string.Format("{0:0.00}", NetProfit));
f.Write("\t" + Trades);
f.Write("\t" + Wins);
f.Write("\t" + Losses);
f.Write("\t" + string.Format("{0:0.00}", Average));
f.Write("\t" + string.Format("{0:0.00}", Win));
f.Write("\t" + string.Format("{0:0.00}", MaxDrawdown));
f.Write("\t" + string.Format("{0:0.00}", GrossProfit));
f.Write("\t" + string.Format("{0:0.00}", GrossLoss));
f.Write("\t" + string.Format("{0:0.00}", ProfitFactor));
f.Write("\t" + string.Format("{0:0.00}", RecoveryFactor));
f.Write("\t" + string.Format("{0:0.00}", Linearity));
f.Write("\t" + string.Format("{0:0.0}", Product));
f.Write("\t" + string.Format("{0:0.0}", ProductDrawdown));
f.WriteLine("");
f.Close();
}
}
}
ИСПОЛЬЗОВАНИЕ
Для использования класса нужно выполнить 4 шага:
1. Скопировать все содержимое приведенной выше подпрограммы в конец своего скрипта (сразу за последней })
2. Внутри класса стратегии объявить переменную data типа SData
3. В конструкторе добавить вызов data.create_file
4. В конце метода стратегии Execute добавить вызов data.write_file
Ниже приведены картики:


Для выполнения оптимизации опционально можно передать в класс SData параметры стратегии и установить фильтры. Об этом чуть ниже.
Генерируемый файл содержит следующие поля:
NetProfit - Сумма процентных приращений всех сделок (выражается тоже в %)
Trades - Количество сделок
Wins- Количество прибыльных сделок
Losses- Количество убыточных сделок
Average- Средняя прибыль на сделку (в %)
Win% - Процент прибыльных сделок
MaxDrawdown- Максимальная просадка по процентным приращениям (в %)
GrossProfit- Сумма процентных приращений всех прибыльных сделок
GrossLoss- Сумма процентных приращений всех убыточных сделок
ProfitFactor- Отношение GrossProfit / GrossLoss (по процентным приращениям)
RecoveryFactor- Отношение NetProfit/MaxDrawdown (по процентным приращениям)
Linearity- Линейность эквити (в относительных единицах. Чем меньше, тем лучше)
Product- Произведение процентных приращений (применяется для оценки реинвестирования с плечом 1:1)
ProductDrawdown- Просадка при реинвестировании (в % от счета)
Описание функций класса:
init_params(sp1, sp2, sp3, …)
– в аргументах принимает список переменных типа StrategyParameter, значения которых будут выводиться в файл наряду со всеми остальными характеристиками системы. Т.е. тут должны быть перечислены все параметры, по которым производится оптимизация. Эта функция вызывается самой первой из класса, в конструкторе стратегии.
create_file(filename, signalname1, signalname2, …)
– создает несколько файлов с именем filename, к каждому из которых добавляется signalnamex. Эта функция вызывается из конструктора стратегии. Например:
create_file(@”C:\my_strategy\data”, “long”, “short”, “”)
создаст три файла:
C:\my_strategy\data_long.txt – сюда будут выводиться результаты только по сделкам с именами “long”
C:\my_strategy\data_short.txt – сюда будут выводиться результаты только по сделкам с именами “short”
C:\my_strategy\data_.txt – сюда будут выводиться результаты по всем сделкам
Примечание: имена сделок задаются средствами WealthLab при открытии позиции, например:
BuyAtClose(bar, “long”)
write_file(Positions)
– данная функция выполняет непосредственно все расчеты по полученному WealthLab’ом списку сделок. Должна вызываться в самом конце метода Execute.
set_limit_xxx(min, max)
– функция установки верхнего и нижнего пределов характеристики стратегии, чтобы исключить запись в файл заведомо неподходящих результатов. Список этих функци приведен ниже:
set_limit_NetProfit
set_limit_Average
set_limit_MaxDrawdown
set_limit_Trades
set_limit_Win
set_limit_ProfitFactor
set_limit_RecoveryFactor
set_limit_Linearity
set_limit_Product
set_limit_ProductDrawdown
Например:
set_limit_NetProfit(0, 10E10) – при выполнении оптимизации исключит все результаты с отрицательным NetProfit’ом.
Вызов этих функций может оказаться полезным, чтобы при большом количестве проходов автоматически сократить список результатов, что упростит их анализ. (10E10 = 10^10 - это заведомо недостижимое число, чтобы не было ограничения сверху)
ОЦЕНКА ЛИНЕЙНОСТИ
Как я уже написал, в качестве примера дополнительных расчетов класс SData содержит код определения прямолинейности эквити (некое подобие коэффициента шарпа). Этот код вычисляет ширину канала, образованного параллельными линиями, охватывающими эквити, и делит его на общий NetProfit. Т.о. чем это значение меньше, тем лучше. Ниже приведены два графика эквити, демонстрирующие полезность этой характеристики:
Обе имеют одинаковый NetProfit. Причем второй график имеет бОльшую просадку, чем первый. Однако коэффициент линейности, вычисленный SData, позволяет определить непригодность первого варианта в торговле из-за длительного (больше 2х лет) нахождения в боковике.


НЕДОСТАТКИ КЛАССА SData
1. Уровень просадки вычисляется только в моменты закрытия сделок. Т.е. если сделка перед тем, как дать прибыль, побывала в просадке, то WL это заметит, а SData такого не вычисляет. Как следствие просадка, вычисляемая SData всегда меньше фактической в моменте. Эта разница тем существеннее, чем больше стопы. На мелких стопах разница будет незначительной.
2. Допуск при расчете параметра Product: при расчете результатов с реинвестированием не учитывается дискретность объемов входа. Поэтому, если первая сделка дает прибыль 1%, то считается, что следующая совершается объемом на 1% больше предыдущего, хотя в реальности на увеличение объема денег может еще не хватать. Так что этот параметр тем ближе к истинному, чем больше начальный капитал.
ПРИМЕР ПРИМЕНЕНИЯ
В качестве примера использования SData приведу одну из своих давнишних заготовок. (Код см. в прикрепленном файле).
Простенькая стратегия, которая входит при пробое линий боллинджера наружу в направлении пробоя. Сразу же выставляются тейк и стоп, которые рассчитываются пропорционально текущей ширине канала боллинджера. Формально стратегия описывается так:
1. Работаем на 15-минутках fRTS
2. Используемый индикатор – линии боллинджера с отклонением 3.0 и параметрическим периодом spPeriod
3. Вход в лонг при пробое верхней линии боллинджера и закрытии выше нее
4. Вход в шорт при пробое нижней линии боллинджера и закрытии ниже нее
5. Тейк и стоп рассчитываются параметрически от ширины канала:
Тейк = spTake * (BBH-BBL)
Стоп = spStop * (BBH-BBL)
6. Если тейк или стоп попадает в область первой свечи дня, то выход из сделки производится по ее закрытию. Это защищает нас от неправильной оценки при гэпах.
(Одна из стратегий, по которым я сейчас торгую, построена именно на этой идее.)
Код стратегии с уже подключенным SData прикреплен. Компилируем, запускаем оптимизацию и смотрим, что получилось. (Следует создать папку, указанную в data.create_file, или изменить в аргументах на свой путь.) Тестирование проводилось на склеенном fRTS 2009-2013.
Как нам помогает код SData.
Во-первых, благодаря установленным фильтрам (см. конструктор и вызовы data.set_limit_xxx) нам придется анализировать не 560 строк результата, а только 200 для лонгов и 90 для шортов, т.к. фильтры убрали заведомо не интересующие нас варианты (xls-файл прилагается)
Во-вторых, благодаря разбивке результатов по сигналам мы можем анализировать отдельно лонг и отдельно шорт, что не лишено смысла, учитывая различную природу поведения роста и падения цены.
В-третьих, мы получили на руки очень важный параметр - коэффициент прямолинейности эквити, по которому мы сможем выбрать самую ровную эквити.
Немного посидев над полученными данными, можно выбрать понравившуюся комбинацию параметров для лонга и шорта в зависимости от собственных предпочтений в комфортности торговли.
Например, для лонга:
Период = 50, Тейк = 1.1, Стоп = 0.3
Для шорта:
Период = 100, Тейк = 0.3, Стоп = 0.3
В результате получим такую эквити:

