Трейдерский Клуб

Общение о рынках, рисках и жизни. Без пиара и без рекламы. Здесь рады только своим.

Код для расширенной оценки результатов оптимизации WealthLab

Код для расширенной оценки результатов оптимизации WealthLab

Сообщение a_krotov » Вт май 14, 2013 10:43 pm

Доброго времени суток сообществу!

Хочу поделиться полезным дополнением к 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

В результате получим такую эквити:
Изображение
Изображение
У вас нет необходимых прав для просмотра вложений в этом сообщении.
a_krotov
 
Сообщения: 9
Зарегистрирован: Сб май 11, 2013 5:24 pm

Re: Код для расширенной оценки результатов оптимизации Wealt

Сообщение dtrader » Ср май 15, 2013 7:36 pm

Кстати, интересный код визуализации сделок! Спасибо.
Забанен за хамство пользователям и споры с администратором.
Аватара пользователя
dtrader
 
Сообщения: 30
Зарегистрирован: Сб мар 09, 2013 6:23 pm
Откуда: Забанен за хамство пользователям и споры с администратором.

Re: Код для расширенной оценки результатов оптимизации Wealt

Сообщение Denis » Вс мар 22, 2015 9:25 pm

Например, для лонга:
Период = 50, Тейк = 1.1, Стоп = 0.3

Для шорта:
Период = 100, Тейк = 0.3, Стоп = 0.3


А как Вы применяете разные параметры для лонга и шорта? В коде стратегии они общие.
Denis
 
Сообщения: 1
Зарегистрирован: Вс мар 15, 2015 11:02 am


Вернуться в PUB: Вопросы и Ответы

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 1

cron