Table of Contents

На истории

Тестирование на исторических данных позволяет проводить как анализ рынка для поиска закономерностей, так и оптимизацию параметров стратегии. Основную работу при этом выполняет класс HistoryEmulationConnector, который получает сохраненные в локальном хранилище данные через специальный API. Дополнительные параметры описаны в разделе настройки тестирования.

Тестирование может выполняться по различным типам маркет-данных:

  • Тиковые сделки (ITickTradeMessage)
  • Стаканы (IOrderBookMessage)
  • Свечи разных таймфреймов
  • OderLog (лог заявок)
  • Level1 (лучшие цены спроса и предложения)
  • Комбинации различных типов данных

Если на период тестирования отсутствуют сохраненные стаканы, они могут быть сгенерированы на основе сделок с помощью MarketDepthGenerator или восстановлены из ордерлога с помощью OrderLogMarketDepthBuilder.

Данные для тестирования на истории должны быть заранее скачаны и сохранены в специальном S# формате. Это можно сделать самостоятельно, используя Коннекторы и Storage API, или настроить и запустить специальную программу Hydra.

Основные этапы тестирования на истории

1. Настройка хранилища данных

Первым шагом необходимо создать объект IStorageRegistry, через который HistoryEmulationConnector будет получать исторические данные:

// хранилище, через которое будет производиться доступ к историческим данным
var storageRegistry = new StorageRegistry
{
    // устанавливаем путь к директории с историческими данными
    DefaultDrive = new LocalMarketDataDrive(HistoryPath.Folder)
};
Caution

В конструктор LocalMarketDataDrive передается путь к корневой директории, где хранится история для всех инструментов, а не к директории с конкретным инструментом. Например, если архив HistoryData.zip был распакован в директорию *C:\R\RIZ2@FORTS\\*, то в LocalMarketDataDrive необходимо передать путь C:\. Подробнее, в разделе API.

2. Создание инструментов и портфелей

// создаем тестовый инструмент, на котором будет производиться тестирование
var security = new Security
{
    Id = SecId.Text, // ID инструмента соответствует имени папки с историческими данными
    Code = secCode,
    Board = board,
};

// тестовый портфель
var portfolio = new Portfolio
{
    Name = "test account",
    BeginValue = 1000000,
};

3. Создание эмуляционного коннектора

// создаем коннектор для эмуляции
var connector = new HistoryEmulationConnector(
    new[] { security },
    new[] { portfolio })
{
    EmulationAdapter =
    {
        Emulator =
        {
            Settings =
            {
                // исполнение заявки, если историческая цена коснулась цены заявки
                // По умолчанию выключено, цена должна пройти сквозь цену заявки
                // (более строгий режим проверки)
                MatchOnTouch = false,
                
                // комиссия для сделок
                CommissionRules = new ICommissionRule[]
                {
                    new CommissionPerTradeRule { Value = 0.01m },
                }
            }
        }
    },
    UseExternalCandleSource = emulationInfo.UseCandle != null,
    CreateDepthFromOrdersLog = emulationInfo.UseOrderLog,
    CreateTradesFromOrdersLog = emulationInfo.UseOrderLog,
    HistoryMessageAdapter =
    {
        StorageRegistry = storageRegistry,
        // установка диапазона тестирования
        StartDate = startTime,
        StopDate = stopTime,
        OrderLogMarketDepthBuilders =
        {
            {
                secId,
                LocalizedStrings.ActiveLanguage == Languages.Russian
                    ? (IOrderLogMarketDepthBuilder)new PlazaOrderLogMarketDepthBuilder(secId)
                    : new ItchOrderLogMarketDepthBuilder(secId)
            }
        }
    },
    // установка интервала обновления времени рынка
    MarketTimeChangedInterval = timeFrame,
};

4. Подписка на события и настройка генерации данных

При подключении настраиваем получение нужных данных в зависимости от параметров тестирования:

connector.SecurityReceived += (subscr, s) =>
{
    if (s != security)
        return;
        
    // заполняем значения Level1
    connector.EmulationAdapter.SendInMessage(level1Info);
    
    // подписываемся на нужные данные в зависимости от настроек тестирования
    if (emulationInfo.UseMarketDepth)
    {
        connector.Subscribe(new(DataType.MarketDepth, security));
        
        // если нужно генерировать стаканы
        if (generateDepths || emulationInfo.UseCandle != null)
        {
            // если нет исторических данных стаканов, но они требуются стратегии,
            // используем генератор на основе последних цен
            connector.RegisterMarketDepth(new TrendMarketDepthGenerator(connector.GetSecurityId(security))
            {
                Interval = TimeSpan.FromSeconds(1), // частота обновления стакана - 1 сек
                MaxAsksDepth = maxDepth,
                MaxBidsDepth = maxDepth,
                UseTradeVolume = true,
                MaxVolume = maxVolume,
                MinSpreadStepCount = 2,
                MaxSpreadStepCount = 5,
                MaxPriceStepCount = 3
            });
        }
    }
    
    if (emulationInfo.UseOrderLog)
    {
        connector.Subscribe(new(DataType.OrderLog, security));
    }
    
    if (emulationInfo.UseTicks)
    {
        connector.Subscribe(new(DataType.Ticks, security));
    }
    
    if (emulationInfo.UseLevel1)
    {
        connector.Subscribe(new(DataType.Level1, security));
    }
    
    // запускаем стратегию перед началом эмуляции
    strategy.Start();
    
    // запускаем загрузку исторических данных
    connector.Start();
};

5. Создание и настройка стратегии

// создаем торговую стратегию на основе скользящих средних с периодами 80 и 10
var strategy = new SmaStrategy
{
    LongSma = 80,
    ShortSma = 10,
    Volume = 1,
    Portfolio = portfolio,
    Security = security,
    Connector = connector,
    LogLevel = DebugLogCheckBox.IsChecked == true ? LogLevels.Debug : LogLevels.Info,
    // по умолчанию интервал 1 мин, что избыточно для диапазона в несколько месяцев
    UnrealizedPnLInterval = ((stopTime - startTime).Ticks / 1000).To<TimeSpan>()
};

// настраиваем тип используемых данных для построения свечей
if (emulationInfo.UseCandle != null)
{
    strategy.CandleType = emulationInfo.UseCandle;
    
    if (strategy.CandleType != TimeSpan.FromMinutes(1).TimeFrame())
    {
        strategy.BuildFrom = TimeSpan.FromMinutes(1).TimeFrame();
    }
}
else if (emulationInfo.UseTicks)
    strategy.BuildFrom = DataType.Ticks;
else if (emulationInfo.UseLevel1)
{
    strategy.BuildFrom = DataType.Level1;
    strategy.BuildField = emulationInfo.BuildField;
}
else if (emulationInfo.UseOrderLog)
    strategy.BuildFrom = DataType.OrderLog;
else if (emulationInfo.UseMarketDepth)
    strategy.BuildFrom = DataType.MarketDepth;

6. Визуализация результатов

Для наглядного отображения результатов тестирования подписываемся на изменения P&L и позиции:

var pnlCurve = equity.CreateCurve(LocalizedStrings.PnL + " " + emulationInfo.StrategyName, Colors.Green, Colors.Red, DrawStyles.Area);
var realizedPnLCurve = equity.CreateCurve(LocalizedStrings.PnLRealized + " " + emulationInfo.StrategyName, Colors.Black, DrawStyles.Line);
var unrealizedPnLCurve = equity.CreateCurve(LocalizedStrings.PnLUnreal + " " + emulationInfo.StrategyName, Colors.DarkGray, DrawStyles.Line);
var commissionCurve = equity.CreateCurve(LocalizedStrings.Commission + " " + emulationInfo.StrategyName, Colors.Red, DrawStyles.DashedLine);

strategy.PnLReceived2 += (s, pf, t, r, u, c) =>
{
    var data = equity.CreateData();

    data
        .Group(t)
        .Add(pnlCurve, r - (c ?? 0))
        .Add(realizedPnLCurve, r)
        .Add(unrealizedPnLCurve, u ?? 0)
        .Add(commissionCurve, c ?? 0);

    equity.Draw(data);
};

var posItems = pos.CreateCurve(emulationInfo.StrategyName, emulationInfo.CurveColor, DrawStyles.Line);

strategy.PositionReceived += (s, p) =>
{
    var data = pos.CreateData();

    data
        .Group(p.LocalTime)
        .Add(posItems, p.CurrentValue);

    pos.Draw(data);
};

// подписываемся на обновление прогресса
connector.ProgressChanged += steps => this.GuiAsync(() => progressBar.Value = steps);

7. Запуск тестирования

// запускаем эмуляцию
connector.Connect();

Современная реализация тестирования на истории

В последних версиях S# пример тестирования на истории был существенно модернизирован и теперь позволяет тестировать стратегию с использованием различных типов маркет-данных:

  • Тики (сделки)
  • Стаканы (книга заявок)
  • Свечи различных таймфреймов
  • Ордерлог (лог заявок)
  • Level1 данные (лучшие цены)
  • Комбинации разных типов данных

Для каждого типа данных создается отдельная вкладка с графиками и статистикой:

// создаем режимы тестирования
_settings = new[]
{
    (
        TicksCheckBox,
        TicksProgress,
        TicksParameterGrid,
        // тики
        new EmulationInfo
        {
            UseTicks = true,
            CurveColor = Colors.DarkGreen,
            StrategyName = LocalizedStrings.Ticks
        },
        TicksChart,
        TicksEquity,
        TicksPosition
    ),

    (
        TicksAndDepthsCheckBox,
        TicksAndDepthsProgress,
        TicksAndDepthsParameterGrid,
        // тики + стаканы
        new EmulationInfo
        {
            UseTicks = true,
            UseMarketDepth = true,
            CurveColor = Colors.Red,
            StrategyName = LocalizedStrings.TicksAndDepths
        },
        TicksAndDepthsChart,
        TicksAndDepthsEquity,
        TicksAndDepthsPosition
    ),
    
    // другие комбинации типов данных
};

Такой подход позволяет наглядно сравнивать результаты работы стратегии при использовании разных источников данных.

Улучшенная стратегия SMA

Стратегия скользящего среднего (SMA) была переработана и теперь использует более современный подход к подписке на данные и обработке свечей:

protected override void OnStarted(DateTimeOffset time)
{
    base.OnStarted(time);

    // создаем подписку на свечи нужного типа
    var dt = CandleTimeFrame is null
        ? CandleType
        : DataType.Create(CandleType.MessageType, CandleTimeFrame);

    var subscription = new Subscription(dt, Security)
    {
        MarketData =
        {
            IsFinishedOnly = true,
            BuildFrom = BuildFrom,
            BuildMode = BuildFrom is null ? MarketDataBuildModes.LoadAndBuild : MarketDataBuildModes.Build,
            BuildField = BuildField,
        }
    };

    // создаем индикаторы
    var longSma = new SMA { Length = LongSma };
    var shortSma = new SMA { Length = ShortSma };

    // подписываемся на свечи и связываем их с индикаторами
    SubscribeCandles(subscription)
        .Bind(longSma, shortSma, OnProcess)
        .Start();

    // настраиваем отображение на графике
    var area = CreateChartArea();

    if (area != null)
    {
        DrawCandles(area, subscription);
        DrawIndicator(area, shortSma, System.Drawing.Color.Coral);
        DrawIndicator(area, longSma);
        DrawOwnTrades(area);
    }

    // настраиваем защиту позиций
    StartProtection(TakeValue, StopValue);
}

Обработка свечей и принятие торговых решений теперь выделено в отдельный метод:

private void OnProcess(ICandleMessage candle, decimal longValue, decimal shortValue)
{
    LogInfo(LocalizedStrings.SmaNewCandleLog, candle.OpenTime, candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice, candle.TotalVolume, candle.SecurityId);

    // проверяем, что свеча завершена
    if (candle.State != CandleStates.Finished)
        return;

    // анализируем пересечение индикаторов
    var isShortLessThenLong = shortValue < longValue;

    if (_isShortLessThenLong == null)
    {
        _isShortLessThenLong = isShortLessThenLong;
    }
    else if (_isShortLessThenLong != isShortLessThenLong) // произошло пересечение
    {
        // если короткая меньше длинной - продаем, иначе покупаем
        var direction = isShortLessThenLong ? Sides.Sell : Sides.Buy;

        // рассчитываем объем для открытия позиции или разворота
        var volume = Position == 0 ? Volume : Position.Abs().Min(Volume) * 2;

        // используем цену закрытия свечи
        var price = candle.ClosePrice;

        if (direction == Sides.Buy)
            BuyLimit(price, volume);
        else
            SellLimit(price, volume);

        _isShortLessThenLong = isShortLessThenLong;
    }
}

Дополнительные настройки тестирования

В S# доступны расширенные настройки для тестирования, включая:

  • Генерация стаканов с заданными параметрами
  • Настройка комиссий
  • Настройка проскальзывания цен
  • Эмуляция задержек исполнения

Более подробно эти настройки описаны в разделе Настройки тестирования.