Автоматизация торговли акциями на ММВБ на примере терминала от Альфа Банка

в 7:33, , рубрики: api, C#, акции, альфа директ, ммвб, разработка, торговые роботы, метки: ,

В свободное от работы время занимаюсь созданием торговых роботов. Тема финансовых рынков и автоматизации торгов меня интересует давно, и сегодня я рад поделиться примером создания простого робота на примере известного биржевого терминала от Альфа Банка.

Предыстория

Многие банки (и другие компании) сейчас предоставляют брокерские услуги, это означает, что заключив дополнительный договор с банком (кроме основного), клиент может инвестировать свои сбережения в различные финансовые инструменты. Довольно давно появились торговые терминалы — программы, через которые клиент банка, пройдя авторизацию, может выставлять заявки, покупать и продавать. Например, акции, фьючерсы, опционы.

Так как рынок находится в постоянном движении, цены меняются. Продав или купив инструмент в нужный момент, можно заработать на разнице курсов. Для того, чтобы человеку не приходилось постоянно находиться у компьютера и следить за ходом торгов, разрабатываются программы-роботы, которые работают по заданному алгоритму — выставляют заявки на покупку и продажу, следят за балансом на счетах и оценивают ситуацию на рынке. Такие роботы настраиваются изначально и затем лишь изредка корректируются человеком, в идеальном случае, конечно. На деле все намного сложнее.

Описание системы

Идея подключения к различным торговым терминалам совсем не новая, но идеально подходит для автоматизации действий пользователя в клиентском банковском ПО. Несмотря на то, что сейчас у меня есть прямой доступ на Московскую биржу по протоколам FIX/FAST, все торговые стратегии я проверяю через банковский терминал, программное взаимодействие с которым в этой статье хочу показать на примере терминала Альфа Директ версии 3.5.

Грубо говоря, задача сводится к следующему (по шагам):

  • Описание интерфейса для взаимодействия с терминалом;
  • Получение позиций и исторических данных с банковского сервера;
  • Тестирование торговых стратегий на исторических данных;
  • Торговля.

Хочу отметить, что существующее решение мной постоянно дорабатывается. Руководствуясь принципом «чем проще, тем лучше», я исключаю многое, что было добавлено ранее (за ненадобностью). Поэтому сейчас система, например, умеет выставлять только лимитированные заявки, которые требуются моим торговым стратегиям. Все остальное легко добавляется по необходимости.

Интерфейс для взаимодействия с банковским терминалом

Для добавления нового способа связи необходимо реализовать следующий интерфейс:

    public interface IIntegrator
    {
        bool Connected {get; set;}
        void Initialize();
        void Connect(string userName, string password);
        void Disconnect();
        void Stop();
        Action UpdateInfo { get; set; }
        bool AllowsHistory { get; }
        bool AllowsTesting { get; }
        bool AllowsTrading { get; }
        bool RequiresCredentials { get; }
        string[] AvaliableMarketCodes { get; }
        //Date, Open, High, Low, Close, Volume, Adj Close
        List<string[]> LoadHistory(string marketNameCode,string instrument,int period,DateTime dateFrom,DateTime dateTo);

        Helpers.Positions[] GetPositions();
        double GetLastPrice(string code);
        int LimitSell(string briefCase, string ticker, string placeCode, int amount, double price, int timeOut, double? activateIfPriceHasGrown, double? activateIfPriceHasFallen);
        int LimitBuy(string briefCase, string ticker, string placeCode, int amount, double price, int timeOut, double? activateIfPriceHasGrown, double? activateIfPriceHasFallen);
        void DropOrder(int orderNo);
    }

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

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

UpdateInfo() вызывается в случае изменения котировок, баланса и любых других данных, это позволяет программе обновить информацию, которую она получает из подключенного модуля.

Дополнительный класс Positions описывает текущие позиции по каждому из инструментов рынка — по портфелям, рынкам, тикерам, собственно, балансу, прибыли и убыткам за день.

public class Positions
    {
        public string Briefcase { get; set; }
        public string MarketCode { get; set; }
        public string Ticker { get; set; }
        public double Amount { get; set; }
        public double DailyPL { get; set; }
    }

Получение позиций и исторических данных

Ниже реализация класса для взаимодействия с терминалом Альфа Директ версии 3.5, в references проекта необходимо добавить COM модуль Interpop.ADLide:

Исходный код для Альфа Директ 3.5

    public class AlfaDirectIntegrator : IIntegrator, INotifyPropertyChanged
    {
        public bool Connected
        {
            get
            {
                if (_terminal == null)
                    return false;
                return _terminal.Connected;
            }
            set
            {
                _terminal.Connected = value;
                OnPropertyChanged("Connected");
                if (UpdateInfo!=null)
                    UpdateInfo();
            }
        }
        public Action UpdateInfo { get; set; }

        private AlfaDirectClass _terminal;

        public bool AllowsHistory
        {
            get
            {
                return true;
            }
        }
        public bool AllowsTesting
        {
            get
            {
                return true;
            }
        }
        public bool AllowsTrading
        {
            get
            {
                return true;
            }
        }
        public bool RequiresCredentials {
            get
            {
                return true;
            }
        }

        // Альфа Директ поддерживает изменение подключения по событию OnConnectionChanged
        // но работает это через не всегда
        // Поэтому дополнительно раз в секунду будем проверять соединение с сервером

        private Timer _connectionStateChecker;
        private int _msToCheckConnectionState = 1000; // интервал времени на проверку

        public void Initialize()
        {
            _terminal = new AlfaDirectClass();
            _connectionStateChecker = new Timer(_msToCheckConnectionState);
            _connectionStateChecker.Elapsed += _connectionStateChecker_Elapsed;
            _terminal.OnConnectionChanged += (obj) =>
            {
                _connectionStateChecker_Elapsed(null, null);
            };
            _connectionStateChecker.Enabled = true;
        }

        void _connectionStateChecker_Elapsed(object sender, ElapsedEventArgs e)
        {
            OnPropertyChanged("Connected");
            UpdateInfo();
        }

        public void Connect(string userName, string password)
        {
            _terminal.UserName = userName;
            _terminal.Password = password;
            Connected = true;
        }

        public void Disconnect()
        {
            Connected = false;
        }

        public void Stop()
        {
            if (_connectionStateChecker != null)
            {
                _connectionStateChecker.Enabled = false;
                _connectionStateChecker.Elapsed -= _connectionStateChecker_Elapsed;
                _connectionStateChecker = null;
            }
            _terminal = null;
        }

        public string[] AvaliableMarketCodes
        {
            get
            {
                return MarketCodes.Keys.ToArray();
            }
        }

        private Dictionary<string, string> MarketCodes = new Dictionary<string, string>() {
            { "МБ ЦК", "MICEX_SHR_T" },
            { "ФОРТС", "FORTS" },
            { "КЦБ ММВБ", "MICEX_SHR"},
            { "FOREX", "CURRENCY"},
            { "DJ Indexes", "DJIA"},
            { "Долг РФ", "EBONDS"},
            { "OTC EUROCLEAR", "EUROTRADE"},
            { "Рос. индексы", "INDEX"},
            { "Межд. индексы", "INDEX2"},
            { "IPE", "IPE"},
            { "LME", "LME"},
            { "LSE", "LSE"},
            { "LSE(delay)", "LSE_DL"},
            { "ГЦБ ММВБ", "MICEX_BOND"},
            { "ВО ММВБ", "MICEX_EBND"},
            { "ВР ММВБ", "MICEX_SELT"},
            { "NEWEX", "NEWEX"},
            { "Альфа-Директ", "NONMARKET"},
            { "Альфа-Директ (ДКК)", "NONMARKET_DCC"},
            { "NYSE", "NYSE"},
            { "ОТС (НДЦ)", "OTC_NDC"},
            { "Газпром (РТС)", "RTS_GAZP"},
            { "РТС СГК", "RTS_SGK_R"},
            { "РТС", "RTS_SHR"},
            { "РТС стд.", "RTS_STANDARD"}
        };

        public List<string[]> LoadHistory(string marketNameCode, string instrument, int period, DateTime dateFrom, DateTime dateTo)
        {
            // Формат даты: 08.07.2010 15:22:21
            // получаем код торговой площадки
            marketNameCode = MarketCodes[marketNameCode];
            List<string[]> data = new List<string[]>();
            var rawData = (string)_terminal.GetArchiveFinInfo(marketNameCode, instrument, period, dateFrom, dateTo.AddDays(1), 2, 100);
            if (_terminal.LastResult != StateCodes.stcSuccess)
            {
                System.Windows.MessageBox.Show(_terminal.LastResultMsg,"",System.Windows.MessageBoxButton.OK,System.Windows.MessageBoxImage.Error);
                return data;
            }
            string[] stringSeparators = new string[] { "rn" };
            // разбиваем по рядам
            var strings = rawData.Split(stringSeparators,StringSplitOptions.None).ToList();
            foreach (var s in strings)
                if (s != "")
                {
                    var values = s.Replace(',','.').Split('|');
                    data.Add(new string[] {values[0] , values[1], values[2], values[3], values[4], values[5], values[4] });
                }
            // получаем данные 
            return data;
        }

        public double GetLastPrice(string code)
        {
            var message = _terminal.GetLocalDBData("FIN_INFO", "last_price", "p_code like '" + code + "'");
            if (message == null)
                return 0;
            return Convert.ToDouble(message.Split('|')[0]);
        }

        public int LimitSell(string briefCase, string ticker, string placeCode, int amount, double price, int timeOut, double? activateIfPriceHasGrown, double? activateIfPriceHasFallen)
        {
            return _terminal.CreateLimitOrder(briefCase, placeCode, ticker, DateTime.Now.AddMinutes(1), " Trade Robot System", "RUR", "S", amount, price, activateIfPriceHasGrown, activateIfPriceHasFallen, null,
                null, null, 'Y', null, null, null, null, null, null, null, null, null, null, timeOut);
        }

        public int LimitBuy(string briefCase, string ticker, string placeCode, int amount, double price, int timeOut, double? activateIfPriceHasGrown, double? activateIfPriceHasFallen)
        {
            return _terminal.CreateLimitOrder(briefCase, placeCode, ticker, DateTime.Now.AddMinutes(1), " Trade Robot System", "RUR", "B", amount, price, activateIfPriceHasGrown, activateIfPriceHasFallen, null,
                null, null, 'Y', null, null, null, null, null, null, null, null, null, null, timeOut);
        }

        public void DropOrder(int orderNo)
        {
            _terminal.DropOrder(orderNo, null, null, null, null, null, 0);
        }

        public Helpers.Positions[] GetPositions()
        {
            var result = new List<Helpers.Positions>();
            var message = _terminal.GetLocalDBData("balance", "acc_code, p_code, place_code, forword_rest, daily_pl", "");
            if ((message == null)||(!Connected))
                return result.ToArray();
            string[] stringSeparators = new string[] { "rn" };
            // разбиваем по рядам
            var strings = message.Split(stringSeparators,StringSplitOptions.None).ToList();
            foreach (var str in strings)
                if (str != "")
                {
                    var fields = str.Split('|');
                    result.Add(new Helpers.Positions()
                    {
                        Briefcase = fields[0],
                        Ticker = fields[1],
                        MarketCode = fields[2],
                        Amount = Convert.ToDouble(fields[3]),
                        DailyPL = Convert.ToDouble(fields[4])
                    });
                }
            return result.ToArray();
        }

        #region PropertyChanged members
        
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion
    }

На самом деле коды рынков можно получить и через сам терминал. У меня возникли проблемы с обработкой событий OnConnectionChanged, поэтому дополнительно пришлось использовать таймер.

Пример загрузки исторических данных

Автоматизация торговли акциями на ММВБ на примере терминала от Альфа Банка - 1

Внизу разными цветами (в зависимости от прибыли или убытка за день) показаны активы — акции и деньги.

Проверка стратегий и торговля

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

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

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

Спасибо за внимание.

Если было интересно, в следующий раз могу рассказать про регрессионный анализ, эконометрику, некоторые нестандартные индикаторы тренда, как получать биржевые данные из интернета, о прямом подключении к биржам и протоколах FIX/FAST.

Автор: AntonioGrande

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js