Для изменения размера нажмите или перетащите
Итерационная модель

Наравне с событийным подходом при создании стратегии, в S# можно создавать код стратегии, основанной на итерационной модели. Такой подход следует использовать, если требуется простая реализация стратегии, не критичная к скорости исполнения.

Для создания стратегии на основе итерационной модели используется класс TimeFrameStrategy. При использовании данного класса основной код торгового алгоритма должен быть сосредоточен в методе OnProcess. Частота вызова данного метода зависит от значения Interval. Подход в использовании TimeFrameStrategy состоит в итерационной обработке: начало метода -> проверка состояния рынка -> регистрация (или отмена) заявок -> конец метода. При таком подходе необходимо хранить состояния, для того, чтобы следующий вызов метода мог получить данные, созданные в предыдущем вызове.

Работу итерационной модели, демонстрирует пример робота торгующего по алгоритму Cкользящая Cредняя (его простой реализации). В нем реализован класс-наследник TimeFrameStrategy, хранящий между вызовами метода OnProcess состояние пересечений скользящих (длинная выше или ниже короткой).

Пример скользящих средних.

Алгоритм Скользящей Cредней:

  1. Для данного алгоритма необходимы исторические данные, поэтому для примера с сайта Финам были скачены 5-минутки по бумаге Лукойл и сохранены в файл LKOH_history.txt.

    Примечание Примечание

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

  2. Формула простой скользящей средней:

    SMA = (Pm + Pm-1 + ... + Pm-n) / n

    Индикаторы реализованы в пространстве имен StockSharp.Algo.Indicators. Подробнее, в разделе Индикаторы.

  3. Код реализации торговой стратегии для скользящей:

    C#
    class SmaStrategy : TimeFrameStrategy
    {
        private readonly CandleManager _candleManager;
        private bool _isShortLessThenLong;
    
        private DateTime _nextTime;
    
        public SmaStrategy(CandleManager candleManager, SimpleMovingAverage longSma, SimpleMovingAverage shortSma, TimeSpan timeFrame)
            : base(timeFrame)
        {
            _candleManager = candleManager;
    
            this.LongSma = longSma;
            this.ShortSma = shortSma;
        }
    
        public SimpleMovingAverage LongSma { get; private set; }
        public SimpleMovingAverage ShortSma { get; private set; }
    
        protected override void OnStarting()
        {
            // запоминаем текущее положение относительно друг друга
            _isShortLessThenLong = this.ShortSma.LastValue < this.LongSma.LastValue;
    
            // вычисляем время окончания текущей пятиминутки
            _nextTime = base.Interval.GetCandleBounds(base.Trader).Max;
    
            base.OnStarting();
        }
    
        protected override ProcessResults OnProcess()
        {
            // если наша стратегия в процессе остановки
            if (base.ProcessState == ProcessStates.Stopping)
            {
                // отменяем активные заявки
                base.CancelActiveOrders();
    
                // так как все активные заявки гарантированно были отменены, то возвращаем ProcessResults.Stop
                return ProcessResults.Stop;
            }
    
            // событие обработки торговой стратегии вызвалось в первый раз, что раньше, чем окончания текущей 5-минутки.
            if (base.Trader.MarketTime < _nextTime)
            {
                // возвращаем ProcessResults.Continue, так как наш алгоритм еще не закончил свою работу, а просто ожидает следующего вызова.
                return ProcessResults.Continue;
            }
    
            // получаем сформированную свечу
            var candle = _candleManager.GetTimeFrameCandle(base.Security, base.TimeFrame, _nextTime - base.TimeFrame);
    
            // если свечи не существует (не было ни одной сделке в тайм-фрейме), то ждем окончания следующей свечи.
            if (candle == null)
            {
                // если прошло больше 10 секунд с момента окончания свечи, а она так и не появилась,
                // значит сделок в прошедшей 5-минутке не было, и переходим на следующую свечу
                if ((base.Trader.MarketTime - _nextTime) > TimeSpan.FromSeconds(10))
                    _nextTime = base.TimeFrame.GetCandleBounds(base.Trader.MarketTime).Max;
    
                return ProcessResults.Continue;
            }
    
            _nextTime += base.TimeFrame;
    
            // вычисляем новое положение относительно друг друга
            var isShortLessThenLong = this.ShortSma.LastValue < this.LongSma.LastValue;
    
            // если произошло пересечение
            if (_isShortLessThenLong != isShortLessThenLong)
            {
                // если короткая меньше чем длинная, то продажа, иначе, покупка.
                var direction = isShortLessThenLong ? OrderDirections.Sell : OrderDirections.Buy;
    
                // создаем заявку
                var order = this.CreateOrder(direction,
                    base.Security.GetMarketPrice(direction), base.Volume);
    
                // регистрируем ее
                base.RegisterOrder(order);
    
                // запоминаем текущее положение относительно друг друга
                _isShortLessThenLong = isShortLessThenLong;
            }
    
            return ProcessResults.Continue;
        }
    }

    В начале метода через свойство StrategyProcessState проверяется, не находится ли стратегия в процессе остановки (например, был вызван метод StrategyStop или произошла ошибка). Если стратегия в процессе остановки, то отменяются все активные заявки через метод StrategyCancelActiveOrders, чтобы предотвратить их активацию по невыгодным ценам. Если необходимо не только отменить заявки, но также и закрыть позицию, то можно воспользоваться методом StrategyHelperClosePosition(Strategy, Decimal).

    Если остановить стратегию невозможно в текущий момент по каким-либо причинам и требуется некоторое время, то необходимо вернуть значение ProcessResultsContinue, и попытаться завершить работу стратегии в следующей итерации вызова OnProcess. Именно поэтому после вызова метода StrategyStop стратегия не сразу меняет свое состояние на ProcessStatesStopped. В случае SmaStrategy такой ситуации быть не может, так как никаких особых ситуаций в реализации скользящей средней нет. Поэтому при остановке стратегии сразу возвращается ProcessResultsStop.

    После проверки идет сам код работы со скользящими. Важно! У класса Strategy есть метод RegisterOrder(Order), который необходимо вызывать вместо регистрации напрямую через шлюз (IConnectorRegisterOrder(Order)). Все сделки, которые произошли по такой заявке, будут перехватываться Strategy, и на основе них будет производиться расчет позиции, проскальзывания, P&L и т.д.. Также, такие заявки и сделки будут добавлятся в коллекции StrategyOrders и StrategyMyTrades, что позволяет просматривать все заявки и сделки, совершенные в рамках работы стратегии.

    Примечание Примечание

    Если необходимо изменить зарегистрированную заявку, то также нужно вызывать метод ReRegisterOrder(Order, Order), а не обращаться напрямую к шлюзу через метод IConnectorReRegisterOrder(Order, Order).

    В самом конце метода возвращается значение ProcessResultsContinue, которое означает, что стратегия не закончила свою работу и необходимо вызвать ее еще раз. Если реализуется какой-либо другой алгоритм, у которого есть критерий завершения (например, набор позиции до определенного значения), то в случае окончания работы такого алгоритма необходимо возвращать значение ProcessResultsStop.

  4. Инициализация самой стратегии и заполнение его историческими данными:

    C#
    // создаем шлюз
    _trader = new QuikTrader(this.Path.Text);
    
    _trader.Connected += () =>
    {
        _candleManager = new CandleManager(_trader);
        foreach (var builder in _candleManager.Sources.OfType<CandleBuilder>())
        {
            builder.IsSyncRegister = true;
        }
    
        _trader.NewPortfolios += portfolios =>
        {
            // выбираем портфель для ММВБ (так как работаем с LKOH)
            _micex = portfolios.First(p => p.Name == account);
    
            TryCreateStrategy();
        };
    
        _trader.NewSecurities += securities => this.GuiAsync(() =>
        {
            // находим нужную бумагу
            _lkoh = securities.FirstOrDefault(s => s.Code == "LKOH");
    
            TryCreateStrategy();
        });
    
        _trader.NewMyTrades += trades => this.GuiAsync(() =>
        {
            if (_strategy != null)
            {
                // найти те сделки, которые совершила стратегия скользящей средней
                trades = trades.Where(t => _strategy.Orders.Any(o => o == t.Order));
    
                _trades.Trades.AddRange(trades);
            }
        });
    
        _candleManager.CandlesStarted += (token, candles) =>
        {
            DrawCandles(candles.Keys);
    
            // если скользящие за сегодняшний день отрисованы, то рисуем в реальном времени текущие скользящие
            if (_isTodaySmaDrawn)
                DrawSma();
        };
        _candleManager.CandlesChanged += (token, candles) => DrawCandles(candles.Keys);
        //_trader.Error += ex => this.Sync(() => MessageBox.Show(this, ex.ToString()));
        _trader.ConnectionError += ex =>
        {
            if (ex != null)
                this.GuiAsync(() => MessageBox.Show(this, ex.ToString()));
        };
    };
    
    _trader.Connect();
    
    ...
    
    private void TryCreateStrategy()
    {
        // если были получены и инструмент, и портфель
        if (_lkoh != null && _micex != null)
        {
            var candles = File.ReadAllLines("LKOH_history.txt").Select(line =>
            {
                var parts = line.Split(',');
                var time = DateTime.ParseExact(parts[0] + parts[1], "yyyyMMddHHmmss", CultureInfo.InvariantCulture);
                return new TimeFrameCandle
                {
                    OpenPrice = parts[2].To<decimal>(),
                    HighPrice = parts[3].To<decimal>(),
                    LowPrice = parts[4].To<decimal>(),
                    ClosePrice = parts[5].To<decimal>(),
                    TimeFrame = _timeFrame,
                    Time = time,
                    TotalVolume = parts[6].To<int>(),
                    Security = _lkoh,
                };
            });
    
            DrawCandles(candles.Cast<Candle>());
    
            // создаем торговую стратегию, скользящие средние на 80 5-минуток и 10 5-минуток
            _strategy = new SmaStrategy(_candleManager, new SimpleMovingAverage { Length = 80 }, new SimpleMovingAverage { Length = 10 }, _timeFrame)
            {
                Volume = 1,
                Security = _lkoh,
                Portfolio = this.Portfolios.SelectedPortfolio,
                Trader = _trader,
            };
            _strategy.Log += OnLog;
            _strategy.NewOrder += OnNewOrder;
            _strategy.PropertyChanged += OnStrategyPropertyChanged;
    
            var index = 0;
    
            // начинаем вычислять скользящие средние
            foreach (var candle in candles)
            {
                _strategy.LongSma.Process((DecimalIndicatorValue)candle.ClosePrice);
                _strategy.ShortSma.Process((DecimalIndicatorValue)candle.ClosePrice);
    
                // если все скользящие сформировались, то начинаем их отрисовывать
                if (index >= _strategy.LongSma.Length)
                    DrawSmaLines(candle.Time);
    
                index++;
    
                _lastCandleTime = candle.Time;
            }
    
            // регистрируем наш тайм-фрейм
            _candleManager.RegisterTimeFrameCandles(_lkoh, _timeFrame);
    
            this.Start.IsEnabled = true;
        }
    }
    Внимание Внимание

    Номер счета, который в примере записан в переменную account, это не логин в Quik, а код клиента. Об особенности портфелей в Quik читайте в соответствующем разделе.

  5. Запуск и остановка торговой стратегии происходит следующим образом:

    C#
    // если скользящие не отрисованы за текущую сессию
    if (!_isTodaySmaDrawn)
    {
        // вычисляем временные отрезки текущей свечи
        var bounds = _timeFrame.GetCandleBounds(_trader);
    
        var candles = _candleManager.GetTimeFrameCandles(_strategy.Security, _timeFrame, new Range<DateTime>(_lastCandleTime + _timeFrame, bounds.Min));
    
        foreach (var candle in candles)
        {
            _strategy.LongSma.Process((DecimalIndicatorValue)candle.ClosePrice);
            _strategy.ShortSma.Process((DecimalIndicatorValue)candle.ClosePrice);
    
            DrawSmaLines(candle.Time);
    
            _lastCandleTime = candle.Time;
        }
    
        _isTodaySmaDrawn = true;
    }
    
    if (_strategy.ProcessState == ProcessStates.Stopped)
        _strategy.Start();
    else
        _strategy.Stop();

    В процессе работы торговой стратегии может возникнуть ошибка. В этом случае Strategy перехватывает ошибку через метод StrategyOnError(Exception), меняется значение StrategyErrorState на LogLevelsError, выводится текст ошибки через событие ILogSourceLog и самостоятельно начинается остановка стратегии.

    Значение LogLevelsWarning предназначено для оповещения о чем-то необычном. Например, вывести пользователю информацию о том, что начался клиринг, или на счету недостаточно, и есть вероятность того, что следующая заявка не сможет быть зарегистрирована.
  6. Отрисовка на графике новых данных линий скользящий и свечей, показывающих тренд:

    C#
    _trader.CandlesStarted += (token, candles) =>
    {
        DrawCandles(candles.Keys);
    
        // если скользящие за сегодняшний день отрисованы, то рисуем в реальном времени текущие скользящие
        if (_isTodaySmaDrawn)
            DrawSma();
    };
    _trader.CandlesChanged += (token, candles) => DrawCandles(candles);
    
    private void DrawCandles(IEnumerable<Candle> candles)
    {
        this.Sync(() => _chart.Candles.DrawCandles(candles));
    }
    
    private void DrawSma()
    {
        // нас не интересует текущая свеча, так как она еще не сформировалась
        // и из нее нельзя брать цену закрытия
    
        // вычисляем временные отрезки текущей свечи
        var bounds = _timeFrame.GetCandleBounds(_trader);
    
        // если появились новые полностью сформированные свечи
        if (_lastCandleTime < bounds.Min)
        {
            // отступ с конца интервала, чтобы не захватить текущую свечу.
            var endOffset = TimeSpan.FromSeconds(1);
    
            bounds = new Range<DateTime>(_lastCandleTime + _timeFrame, bounds.Min - endOffset);
    
            // получаем эти свечи
            var candles = _trader.GetTimeFrameCandles(_strategy.Security, _timeFrame, bounds);
    
            if (candles.Count() > 0)
            {
                foreach (var candle in candles)
                {
                      // добавляем новую свечу
                     _strategy.LongSma.Process((DecimalIndicatorValue)candle.ClosePrice);
                     _strategy.ShortSma.Process((DecimalIndicatorValue)candle.ClosePrice);
                }
    
                // получаем время самой последней свечи и запоминаем его как новое начало
                _lastCandleTime = candles.Max(c => c.Time);
    
                DrawSmaLines(bounds.Min);
            }
        }
    }
    
    private void DrawSmaLines(DateTime time)
    {
        this.GuiSync(() =>
        {
            _longSmaGraph.Add(new CustomChartIndicator
            {
                Time = time,
                Value = (double)_strategy.LongSma.LastValue
            });
            _shortSmaGraph.Add(new CustomChartIndicator
            {
                Time = time,
                Value = (double)_strategy.ShortSma.LastValue
            });
        });
    }
Следующие шаги