На истории
Тестирование на исторических данных позволяет проводить как анализ рынка для поиска закономерностей, так и оптимизацию параметров стратегии. Вся работа при этом заключена в классе HistoryEmulationConnector (подробнее про настройки тестирования), который получает сохраненные в локальном хранилище данные через специальный API. Тестирование идет по тиковым сделкам (Trade) и стаканам (MarketDepth). Если на период в истории нет сохраненных стаканов, то они генерируются на основе сделок с помощью MarketDepthGenerator.
Данные для тестирования на истории должны быть заранее скачаны и сохранены в специальном S# формате. Это можно сделать самостоятельно, используя Коннекторы и Storage API, или настроить и запустить специальную программу Hydra.
В дистрибутиве S# находится пример SampleHistoryTesting (а также архив HistoryData.zip, где лежат исторические данные по тикам, стаканам и свечам, для примера), который тестирует стратегию Cкользящая Cредняя на истории. Для сравнения скорости и качества, тестирование идет с различным набором маркет-данных:
Тестирование на истории стратегии скользящих средних из примеров SampleSMA и SampleSmartSMA
В начале необходимо создать настройки для тестирования:
var settings = new[] { Tuple.Create( TicksCheckBox, TicksProgress, TicksParameterGrid, // ticks new EmulationInfo {UseTicks = true, CurveColor = Colors.DarkGreen, StrategyName = LocalizedStrings.Ticks}, TicksChart, TicksEquity, TicksPosition), Tuple.Create( TicksAndDepthsCheckBox, TicksAndDepthsProgress, TicksAndDepthsParameterGrid, // ticks + order book new EmulationInfo {UseTicks = true, UseMarketDepth = true, CurveColor = Colors.Red, StrategyName = LocalizedStrings.XamlStr757}, TicksAndDepthsChart, TicksAndDepthsEquity, TicksAndDepthsPosition), Tuple.Create( DepthsCheckBox, DepthsProgress, DepthsParameterGrid, // order book new EmulationInfo {UseMarketDepth = true, CurveColor = Colors.OrangeRed, StrategyName = LocalizedStrings.MarketDepths}, DepthsChart, DepthsEquity, DepthsPosition), Tuple.Create( CandlesCheckBox, CandlesProgress, CandlesParameterGrid, // candles new EmulationInfo {UseCandleTimeFrame = timeFrame, CurveColor = Colors.DarkBlue, StrategyName = LocalizedStrings.Candles}, CandlesChart, CandlesEquity, CandlesPosition), Tuple.Create( CandlesAndDepthsCheckBox, CandlesAndDepthsProgress, CandlesAndDepthsParameterGrid, // candles + orderbook new EmulationInfo {UseMarketDepth = true, UseCandleTimeFrame = timeFrame, CurveColor = Colors.Cyan, StrategyName = LocalizedStrings.XamlStr635}, CandlesAndDepthsChart, CandlesAndDepthsEquity, CandlesAndDepthsPosition), Tuple.Create( OrderLogCheckBox, OrderLogProgress, OrderLogParameterGrid, // order log new EmulationInfo {UseOrderLog = true, CurveColor = Colors.CornflowerBlue, StrategyName = LocalizedStrings.OrderLog}, OrderLogChart, OrderLogEquity, OrderLogPosition), Tuple.Create( Level1CheckBox, Level1Progress, Level1ParameterGrid, // order log new EmulationInfo {UseLevel1 = true, CurveColor = Colors.Aquamarine, StrategyName = LocalizedStrings.Level1}, Level1Chart, Level1Equity, Level1Position), Tuple.Create( FinamCandlesCheckBox, FinamCandlesProgress, FinamCandlesParameterGrid, // candles new EmulationInfo { UseCandleTimeFrame = timeFrame, CustomHistoryAdapter = g => new FinamMessageAdapter(g), CurveColor = Colors.DarkBlue, StrategyName = LocalizedStrings.FinamCandles }, FinamCandlesChart, FinamCandlesEquity, FinamCandlesPosition), Tuple.Create( YahooCandlesCheckBox, YahooCandlesProgress, YahooCandlesParameterGrid, // candles new EmulationInfo { UseCandleTimeFrame = timeFrame, CustomHistoryAdapter = g => new YahooMessageAdapter(g), CurveColor = Colors.DarkBlue, StrategyName = LocalizedStrings.YahooCandles }, YahooCandlesChart, YahooCandlesEquity, YahooCandlesPosition), };
Далее, создать объект IStorageRegistry, через который HistoryEmulationConnector будет получать исторические данные:
// хранилище, через которое будет производиться доступ к тиковой и котировочной базе var storageRegistry = new StorageRegistry { // set historical path DefaultDrive = new LocalMarketDataDrive(HistoryPath.Folder) };
Caution
В конструктор LocalMarketDataDrive передается путь к директории, где лежит история для всех инструментов, а не к директории с конкретным инструментом. Например, если архив HistoryData.zip был распакован в директорию *C:\R\RIZ2@FORTS\\*, то в LocalMarketDataDrive необходимо передать путь C:\. Подробнее, в разделе API.
Далее в цикле создаются инструмент, портфель, стратегия, шлюз для тестирования и т.д. с соответствующими настройками, в зависимости от значений флагов, установленных в главном окне (Тики, Тики и стаканы, Свечи и пр.). Если значение флага False, то программа переходит к следующему набору настроек.
foreach (var set in settings) { if (set.Item1.IsChecked == false) continue; ................. }
Создаем инструменты и портфели, по которым будет производиться тестирование:
// создаем тестовый инструмент, на котором будет производится тестирование var security = new Security { Id = SecId.Text, // sec id has the same name as folder with historical data Code = secCode, Board = board, }; // тестовый портфель var portfolio = new Portfolio { Name = "test account", BeginValue = 1000000, };
Создание самого HistoryEmulationConnector, куда передаются инструменты, портфели, интерфейс хранилища IStorageRegistry, а также настройки тестирования:
// создаем шлюз для эмуляции // инициализируем настройки (инструмент в истории обновляется раз в секунду) var connector = new HistoryEmulationConnector( new[] { security }, new[] { portfolio }) { EmulationAdapter = { Emulator = { Settings = { // match order if historical price touched our limit order price. // It is terned off, and price should go through limit order price level // (more "severe" test mode) MatchOnTouch = false, } } }, UseExternalCandleSource = emulationInfo.UseCandleTimeFrame != null, CreateDepthFromOrdersLog = emulationInfo.UseOrderLog, CreateTradesFromOrdersLog = emulationInfo.UseOrderLog, HistoryMessageAdapter = { StorageRegistry = storageRegistry, // set history range StartDate = startTime, StopDate = stopTime, OrderLogMarketDepthBuilders = { { secId, LocalizedStrings.ActiveLanguage == Languages.Russian ? (IOrderLogMarketDepthBuilder)new PlazaOrderLogMarketDepthBuilder(secId) : new ItchOrderLogMarketDepthBuilder(secId) } } }, // set market time freq as time frame MarketTimeChangedInterval = timeFrame, };
В событии получения нового инструмента задаем начальные значения Level1, регистрируем стакан или создаем и настраиваем генератор стакана. Также в зависимости от настроек регистрируем получение ордерлога и сделок. Запускаем стратегию и генерацию свечей. А также запускаем сам эмулятор.
connector.NewSecurity += s => { if (s != security) return; // fill level1 values connector.HistoryMessageAdapter.SendOutMessage(level1Info); if (emulationInfo.HistorySource != null) { if (emulationInfo.UseCandleTimeFrame != null) { connector.RegisterHistorySource(security, MarketDataTypes.CandleTimeFrame, emulationInfo.UseCandleTimeFrame.Value, emulationInfo.HistorySource); } if (emulationInfo.UseTicks) { connector.RegisterHistorySource(security, MarketDataTypes.Trades, null, emulationInfo.HistorySource); } if (emulationInfo.UseLevel1) { connector.RegisterHistorySource(security, MarketDataTypes.Level1, null, emulationInfo.HistorySource); } if (emulationInfo.UseMarketDepth) { connector.RegisterHistorySource(security, MarketDataTypes.MarketDepth, null, emulationInfo.HistorySource); } } else { if (emulationInfo.UseMarketDepth) { connector.SubscribeMarketDepth(security); if ( // if order book will be generated generateDepths || // of backtesting will be on candles emulationInfo.UseCandleTimeFrame != TimeSpan.Zero ) { // if no have order book historical data, but strategy is required, // use generator based on last prices connector.MarketDataAdapter.SendInMessage(new GeneratorMessage { IsSubscribe = true, Generator = new RandomWalkTradeGenerator(new SecurityId { SecurityCode = security.Code }) { Interval = TimeSpan.FromSeconds(1), MaxVolume = maxVolume, MaxPriceStepCount = 3, GenerateOriginSide = true, MinVolume = minVolume, RandomArrayLength = 99, } }); } } if (emulationInfo.UseOrderLog) { connector.SubscribeOrderLog(security); } if (emulationInfo.UseTicks) { connector.SubscribeTrades(security); } if (emulationInfo.UseLevel1) { connector.SubscribeLevel1(security); } } // start strategy before emulation started strategy.Start(); _series = new CandleSeries(typeof(TimeFrameCandle), security, timeFrame); connector.SubscribeCandles(series); // start historical data loading when connection established successfully and all data subscribed connector.Start(); };
Подключение:
_connector.Connect();
Для переданных в конструктор HistoryEmulationConnector инструментов и портфелей вызываются Connector.NewSecurity и IPortfolioProvider.NewPortfolio.
Создание самой стратегии Cкользящая Cредняя:
// создаем торговую стратегию, скользящие средние на 80 5-минуток и 10 5-минуток var strategy = new SmaStrategy(chart, _candlesElem, _tradesElem, _shortMa, _shortElem, _longMa, _longElem, _series) { Volume = 1, Portfolio = portfolio, Security = security, Connector = connector, LogLevel = DebugLogCheckBox.IsChecked == true ? LogLevels.Debug : LogLevels.Info, // by default interval is 1 min, // it is excessively for time range with several months UnrealizedPnLInterval = ((stopTime - startTime).Ticks / 1000).To<TimeSpan>() };
Подписка на событие Strategy.PnLChanged, для расчета кривой эквити, а также визуальное наблюдение за прогрессом тестирования (в примере используются элементы в виде полос прогресса):
// копируем параметры на визуальную панель statistic.Parameters.Clear(); statistic.Parameters.AddRange(strategy.StatisticManager.Parameters); var equity = set.Item6; var pnlCurve = equity.CreateCurve(LocalizedStrings.PnL + " " + emulationInfo.StrategyName, emulationInfo.CurveColor, ChartIndicatorDrawStyles.Area); var unrealizedPnLCurve = equity.CreateCurve(LocalizedStrings.PnLUnreal + emulationInfo.StrategyName, Colors.Black, ChartIndicatorDrawStyles.Line); var commissionCurve = equity.CreateCurve(LocalizedStrings.Str159 + " " + emulationInfo.StrategyName, Colors.Red, ChartIndicatorDrawStyles.DashedLine); var posItems = set.Item7.CreateCurve(emulationInfo.StrategyName, emulationInfo.CurveColor, ChartIndicatorDrawStyles.Line); strategy.PnLChanged += () => { var pnl = new EquityData { Time = strategy.CurrentTime, Value = strategy.PnL - strategy.Commission ?? 0 }; var unrealizedPnL = new EquityData { Time = strategy.CurrentTime, Value = strategy.PnLManager.UnrealizedPnL ?? 0 }; var commission = new EquityData { Time = strategy.CurrentTime, Value = strategy.Commission ?? 0 }; pnlCurve.Add(pnl); unrealizedPnLCurve.Add(unrealizedPnL); commissionCurve.Add(commission); }; strategy.PositionChanged += () => posItems.Add(new EquityData { Time = strategy.CurrentTime, Value = strategy.Position }); var nextTime = startTime + progressStep; // handle historical time for update ProgressBar connector.MarketTimeChanged += d => { if (connector.CurrentTime < nextTime && connector.CurrentTime < stopTime) return; var steps = (connector.CurrentTime - startTime).Ticks / progressStep.Ticks + 1; nextTime = startTime + (steps * progressStep.Ticks).To<TimeSpan>(); this.GuiAsync(() => progressBar.Value = steps); };
Запуск начала тестирования:
// запускаем эмуляцию foreach (var connector in _connectors) { connector.Connect(); // устанавливаем комиссию connector.SendInMessage(new CommissionRuleMessage { Rule = new CommissionPerTradeRule { Value = 0.01m } }); }