Итерационная модель
Наравне с событийным подходом при создании стратегии, в S# можно создавать код стратегии, основанной на итерационной модели. Такой подход следует использовать, если требуется простая реализация стратегии, не критичная к скорости исполнения.
Для создания стратегии на основе итерационной модели используется класс TimeFrameStrategy. При использовании данного класса основной код торгового алгоритма должен быть сосредоточен в методе TimeFrameStrategy.OnProcess. Частота вызова данного метода зависит от значения TimeFrameStrategy.Interval. Подход в использовании TimeFrameStrategy состоит в итерационной обработке: начало метода -> проверка состояния рынка -> регистрация (или отмена) заявок -> конец метода. При таком подходе необходимо хранить состояния, для того, чтобы следующий вызов метода мог получить данные, созданные в предыдущем вызове.
Работу итерационной модели, демонстрирует пример робота торгующего по алгоритму Cкользящая Cредняя (его простой реализации). В нем реализован класс-наследник TimeFrameStrategy, хранящий между вызовами метода TimeFrameStrategy.OnProcess состояние пересечений скользящих (длинная выше или ниже короткой).
Алгоритм Скользящей Средней:
Для данного алгоритма необходимы исторические данные, поэтому для примера с сайта Финам были скачаны 5-минутки по бумаге Лукойл и сохранены в файл LKOH_history.txt.
Tip
Перед запуском программы рекомендуется обновить данные, скачав их с сайта. Правильный формат данных можно посмотреть в тестовом файле.
Формула простой скользящей средней: *SMA = (Pmm + Pm-1m-1 + ... + Pm-nm-n) / n *
Индикаторы реализованы в пространстве имен StockSharp.Algo.Indicators. Подробнее, в разделе Индикаторы.
Код реализации торговой стратегии для скользящей:
class SmaStrategy : Strategy { private readonly Connector _connector; private readonly CandleSeries _series; private bool _isShortLessThenLong; public SmaStrategy(CandleSeries series, SimpleMovingAverage longSma, SimpleMovingAverage shortSma) { _series = series; _connector = ((Connector)this.Connector); LongSma = longSma; ShortSma = shortSma; } public SimpleMovingAverage LongSma { get; } public SimpleMovingAverage ShortSma { get; } protected override void OnStarted() { _connector .WhenCandlesFinished(_series) .Do(ProcessCandle) .Apply(this); // запоминаем текущее положение относительно друг друга _isShortLessThenLong = ShortSma.GetCurrentValue() < LongSma.GetCurrentValue(); base.OnStarted(); } private void ProcessCandle(Candle candle) { // если наша стратегия в процессе остановки if (ProcessState == ProcessStates.Stopping) { // отменяем активные заявки CancelActiveOrders(); return; } // добавляем новую свечу LongSma.Process(candle); ShortSma.Process(candle); // вычисляем новое положение относительно друг друга var isShortLessThenLong = ShortSma.GetCurrentValue() < LongSma.GetCurrentValue(); // если произошло пересечение if (_isShortLessThenLong != isShortLessThenLong) { // если короткая меньше чем длинная, то продажа, иначе, покупка. var direction = isShortLessThenLong ? Sides.Sell : Sides.Buy; // вычисляем размер для открытия или переворота позы var volume = Position == 0 ? Volume : Position.Abs() * 2; // регистрируем заявку (обычным способом - лимитированной заявкой) //RegisterOrder(this.CreateOrder(direction, (decimal)Security.GetCurrentPrice(direction), volume)); // переворачиваем позицию через котирование var strategy = new MarketQuotingStrategy(direction, volume); ChildStrategies.Add(strategy); // запоминаем текущее положение относительно друг друга _isShortLessThenLong = isShortLessThenLong; } } }
В начале метода через свойство Strategy.ProcessState проверяется, не находится ли стратегия в процессе остановки (например, был вызван метод Strategy.Stop или произошла ошибка). Если стратегия в процессе остановки, то отменяются все активные заявки через метод Strategy.CancelActiveOrders, чтобы предотвратить их активацию по невыгодным ценам. Если необходимо не только отменить заявки, но также и закрыть позицию, то можно воспользоваться методом StrategyHelper.ClosePosition(StockSharp.Algo.Strategies.Strategy strategy, System.Decimal slippage ).
Если остановить стратегию невозможно в текущий момент по каким-либо причинам и требуется некоторое время, то необходимо вернуть значение ProcessResults.Continue, и попытаться завершить работу стратегии в следующей итерации вызова TimeFrameStrategy.OnProcess. Именно поэтому после вызова метода Strategy.Stop стратегия не сразу меняет свое состояние на ProcessStates.Stopped. В случае SmaStrategy такой ситуации быть не может, так как никаких особых ситуаций в реализации скользящей средней нет. Поэтому при остановке стратегии сразу возвращается ProcessResults.Stop.
После проверки идет сам код работы со скользящими. Важно! У класса Strategy есть метод Strategy.RegisterOrder(StockSharp.BusinessEntities.Order order ), который необходимо вызывать вместо регистрации напрямую через шлюз (Connector.RegisterOrder(StockSharp.BusinessEntities.Order order )). Все сделки, которые произошли по такой заявке, будут перехватываться Strategy, и на основе них будет производиться расчет позиции, проскальзывания, P&L и т.д.. Также, такие заявки и сделки будут добавляется в коллекции Strategy.Orders и Strategy.MyTrades, что позволяет просматривать все заявки и сделки, совершенные в рамках работы стратегии.
Tip
Если необходимо изменить зарегистрированную заявку, то также нужно вызывать метод Strategy.ReRegisterOrder(StockSharp.BusinessEntities.Order oldOrder, StockSharp.BusinessEntities.Order newOrder ), а не обращаться напрямую к шлюзу через метод Connector.ReRegisterOrder(StockSharp.BusinessEntities.Order oldOrder, StockSharp.BusinessEntities.Order newOrder ).
В самом конце метода возвращается значение ProcessResults.Continue, которое означает, что стратегия не закончила свою работу и необходимо вызвать ее еще раз. Если реализуется какой-либо другой алгоритм, у которого есть критерий завершения (например, набор позиции до определенного значения), то в случае окончания работы такого алгоритма необходимо возвращать значение ProcessResults.Stop.
Инициализация самой стратегии и заполнение его историческими данными:
_connector.Connected += () => { _connector.NewSecurity += security => { if (!security.Code.CompareIgnoreCase("LKOH")) return; // находим нужную бумагу var lkoh = security; _lkoh = lkoh; this.GuiAsync(() => { Start.IsEnabled = true; }); }; _connector.NewMyTrade += trade => { if (_strategy != null) { // найти те сделки, которые совершила стратегия скользящей средней if (_strategy.Orders.Contains(trade.Order)) Trades.Trades.Add(trade); } }; _connector.CandleSeriesProcessing += (series, candle) => { // если скользящие за сегодняшний день отрисованны, то рисуем в реальном времени текущие скользящие if (_isTodaySmaDrawn && candle.State == CandleStates.Finished) ProcessCandle(candle); }; //_connector.Error += ex => this.GuiAsync(() => MessageBox.Show(this, ex.ToString())); _connector.ConnectionError += ex => { if (ex != null) this.GuiAsync(() => MessageBox.Show(this, ex.ToString())); }; this.GuiAsync(() => { ConnectBtn.IsEnabled = false; Report.IsEnabled = true; }); }; ... private void StartClick(object sender, RoutedEventArgs e) { if (_strategy == null) { if (Portfolios.SelectedPortfolio == null) { MessageBox.Show(this, LocalizedStrings.Str3009); return; } // регистрируем наш тайм-фрейм var series = new CandleSeries(typeof(TimeFrameCandle), _lkoh, _timeFrame); // создаем торговую стратегию, скользящие средние на 80 5-минуток и 10 5-минуток _strategy = new SmaStrategy(series, new SimpleMovingAverage { Length = 80 }, new SimpleMovingAverage { Length = 10 }) { Volume = 1, Security = _lkoh, Portfolio = Portfolios.SelectedPortfolio, Connector = _connector, }; _strategy.Log += OnLog; _strategy.PropertyChanged += OnStrategyPropertyChanged; _candlesElem = new ChartCandleElement(); _area.Elements.Add(_candlesElem); _longMaElem = new ChartIndicatorElement { Title = LocalizedStrings.Long, Color = Colors.OrangeRed }; _area.Elements.Add(_longMaElem); _shortMaElem = new ChartIndicatorElement { Title = LocalizedStrings.Short, Color = Colors.RoyalBlue }; _area.Elements.Add(_shortMaElem); IEnumerable<Candle> candles = CultureInfo.InvariantCulture.DoInCulture(() => File.ReadAllLines("LKOH_history.txt").Select(line => { var parts = line.Split(','); var time = (parts[0] + parts[1]).ToDateTime("yyyyMMddHHmmss").ApplyTimeZone(TimeHelper.Moscow); return (Candle)new TimeFrameCandle { OpenPrice = parts[2].To<decimal>(), HighPrice = parts[3].To<decimal>(), LowPrice = parts[4].To<decimal>(), ClosePrice = parts[5].To<decimal>(), TimeFrame = _timeFrame, OpenTime = time, CloseTime = time + _timeFrame, TotalVolume = parts[6].To<decimal>(), Security = _lkoh, State = CandleStates.Finished, }; }).ToArray()); var lastCandleTime = default(DateTimeOffset); // начинаем вычислять скользящие средние foreach (var candle in candles) { ProcessCandle(candle); lastCandleTime = candle.OpenTime; } _connector.SubscribeCandles(_candleSeries, DateTime.Today.Subtract(TimeSpan.FromDays(30)), DateTime.Now); ...
Запуск и остановка торговой стратегии происходит следующим образом:
... if (_strategy.ProcessState == ProcessStates.Stopped) { // запускаем процесс получения стакана, необходимый для работы алгоритма котирования _connector.SubscribeMarketDepth(_strategy.Security); _strategy.Start(); Start.Content = LocalizedStrings.Str242; } else { _connector.UnSubscribeMarketDepth(_strategy.Security); _strategy.Stop(); Start.Content = LocalizedStrings.Str2421; } ...
В процессе работы торговой стратегии может возникнуть ошибка. В этом случае Strategy перехватывает ошибку через метод Strategy.OnError(StockSharp.Algo.Strategies.Strategy strategy, System.Exception error ), меняется значение Strategy.ErrorState на LogLevels.Error, выводится текст ошибки через событие ILogSource.Log и самостоятельно начинается остановка стратегии.
Tip
Значение LogLevels.Warning предназначено для оповещения о чем-то необычном. Например, вывести пользователю информацию о том, что начался клиринг, или на счету недостаточно, и есть вероятность того, что следующая заявка не сможет быть зарегистрирована.
Отрисовка на графике новых данных линий скользящий и свечей, показывающих тренд:
// начинаем вычислять скользящие средние foreach (var candle in candles) { ProcessCandle(candle); lastCandleTime = candle.OpenTime; } _connector.Start(series); // вычисляем временные отрезки текущей свечи var bounds = _timeFrame.GetCandleBounds(_connector.CurrentTime); candles = _connector.GetCandles(series, new Range<DateTimeOffset>(lastCandleTime + _timeFrame, bounds.Min)); foreach (var candle in candles) { ProcessCandle(candle); } ... private void ProcessCandle(Candle candle) { var longValue = candle.State == CandleStates.Finished ? _strategy.LongSma.Process(candle) : null; var shortValue = candle.State == CandleStates.Finished ? _strategy.ShortSma.Process(candle) : null; var chartData = new ChartDrawData(); chartData .Group(candle.OpenTime) .Add(_candlesElem, candle) .Add(_longMaElem, longValue) .Add(_shortMaElem, shortValue); Chart.Draw(chartData); }