Пишем торговых роботов с помощью графического фреймворка StockSharp. Часть 1

f-uoa9b5v33o_a22qip5tgvjg6o.png

В нашем блоге мы много пишем о технологиях и полезных инструментах, связанных с биржевой торговлей. Один из них — бесплатная платформа StockSharp, которую можно использовать для профессиональной разработки торговых терминалов и торговых роботов на языке C#. В данной статье мы покажем, как использовать графический фреймворк, входящий в S#.API, с целью создания торгового терминала с возможностью запуска алгоритмических стратегий.

Что понадобится


  1. Visual Studio 2017 (Community, бесплатная версия), в ней мы будем программировать.
  2. Подключение к торгам на бирже, в примерах в данном тексте используется интерфейс SMARTcom от ITI Capital.


Создание проекта


Создадим новое WPF-приложение в Visual Studio:

nqj3sbywjx9_xny7tjyac_kn3pc.png

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

Так как все графические элементы S#.API созданы на базе DevExpress, а библиотеки DevExpress идут вместе с S#.API, глупо будет ими не воспользоваться. Перейдем в редактор окна MainWindow.xaml:

niq2zzalyspdk8ztdf9uatdfnps.png

Заменим Window на DXWindow, это нам понадобится для использования разных цветовых схем:

okjoh1ouxpadw48b2scjw8x1df4.png

Visual Studio нам сама предложит вставить необходимые библиотеки.

Разобьем окно на три части — сверху будет полоса с кнопками настройки подключений и подключения, внизу — окно с логами, а в середине все остальные панели. Проще всего так разбить окно с помощью LayoutControl от DevExpress.

В получившиеся три части мы и будем добавлять необходимые нам элементы.


        
                
                        
                
                
                        
                
                
                        
                
        


Настройка подключения к коннектору


Добавим две кнопки, одна кнопка настройки подключения, а вторая кнопка подключения. Для этого воспользуемся кнопкой SimpleButton от DevExpress. Кнопки будут расположены в верхней части приложения. В каждую кнопку поместим картинки, привычные по S#.Designer, S#.Data и S#.Terminal.


        
                
                        
                        
                                
                                        
                                
                        
                        
                                
                                        
                                
                        
                
                
                        
                
                
                        
                
        


В верхнем правом углу экранной формы увидим такую картину:

6wyowd9ztiixrpaygfcs-v6eaj4.png

Двойным кликом на каждую кнопку создадим обработчики событий нажатия на кнопку. В коде MainWindow необходимо объявить коннектор, а также место и имя файла в котором будут храниться настройки коннектора.

public readonly Connector Connector;
private const string _dir = "Data";
private static readonly string _settingsFile = $@"{_dir}\connection.xml";


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

private void SettingsButton_Click(object sender, RoutedEventArgs e)
{
        if (Connector.Configure(this))
        {
                new XmlSerializer().Serialize(Connector.Save(), _settingsFile);
        }
}


В конструкторе будем проверять, есть ли каталог и файл с настройками коннектора, и если он есть, будем его загружать в коннектор:

//----------------------------------------------------------------------------------
Directory.CreateDirectory(_dir);
Connector = new Connector();
if (File.Exists(_settingsFile))
{
        Connector.Load(new XmlSerializer().Deserialize(_settingsFile));
}
//----------------------------------------------------------------------------------


Большинство объектов S#.API имеют методы Save и Load, с помощью которых можно сохранить и загрузить этот объект из XML-файла.

В методе обработчике нажатия на кнопку подключения подключаем коннектор.

          private void ConnectButton_Click(object sender, RoutedEventArgs e)
                {
                        Connector.Connect();
                }


Теперь можно запустить программу и проверить ее.

Установка темной темы


Многие трейдеры предпочитают темные темы торговых приложений. Поэтому сразу делаем так, чтобы тема программы была темной. Для нужно найти файл App.xaml:

dcelxz29jsvbpysoimfdml5lp1m.png

И заменить в нем Application на charting: ExtendedBaseApplication, и Visual Studio нам сама предложит вставить необходимые библиотеки.




А в файле App.xaml.cs нужно удалить »: Application».

namespace ShellNew
{
        /// 
        /// Interaction logic for App.xaml
        /// 
        public partial class App 
        {
        }
}


В конструкторе MainWindow пишем ApplicationThemeHelper.ApplicationThemeName = Theme.VS2017DarkName;

Полный код на текущий момент:

public partial class MainWindow
{
        public readonly Connector Connector;

        private const string _dir = "Data";
        private static readonly string _settingsFile = $@"{_dir}\connection.xml";

        public MainWindow()
        {
                //----------------------------------------------------------------------------------
                ApplicationThemeHelper.ApplicationThemeName = Theme.VS2017DarkName;
                //----------------------------------------------------------------------------------
                Directory.CreateDirectory(_dir);
                Connector = new Connector();
                if (File.Exists(_settingsFile))
                {
                        Connector.Load(new XmlSerializer().Deserialize(_settingsFile));
                }
                //----------------------------------------------------------------------------------
                InitializeComponent();
        }

        private void SettingsButton_Click(object sender, RoutedEventArgs e)
        {
                if (Connector.Configure(this))
                {
                        new XmlSerializer().Serialize(Connector.Save(), _settingsFile);
                }
        }

        private void ConnectButton_Click(object sender, RoutedEventArgs e)
        {
                Connector.Connect();
        }
}


Запускаем для проверки темной темы:

mklh3vcxuf88y7zil2ub58n_js4.png

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


Добавим папку, где мы будем хранить все созданные нами контроллы, и назовем ее XAML. Добавим в нее свой первый UserControll, дадим ему имя SecurityGridControl.

y9yeoioudundf5rke7k2vwh9wau.png

В него добавляем один элемент SecurityPicker. В нем будут отображаться имеющиеся инструменты. По аналогии с главным окном будем использовать LayoutControl от DevExpress.


        


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


        
        
                
        


Теперь, когда у нас есть панель инструментов, надо задать ей источник данных, в нашем случае это коннектор. Можно было просто в конструкторе MainWindow написать
SecurityPanel.SecPicker.SecurityProvider = Connector;
.

Но не стоит засорять MainWindow кодом, который к нему не относится. Поэтому создадим статическую переменную Instance, а в конструкторе MainWindow присвою ему значение MainWindow:

…
public static MainWindow Instance;
…
Instance = this;
…


Теперь в любом месте нашей программы мы можем обращаться к свойствам MainWindow через код MainWindow.Instance.XXX.

В конструкторе SecurityGridControl таким образом указываем Connector как источник данных:

public SecurityGridControl()
{
        InitializeComponent();
        SecPicker.SecurityProvider = MainWindow.Instance.Connector;
}


Запустим для проверки:

kvycezcrwoqeghzdaw2puoagmmc.png

Добавление логирования


Работу программы, коннектора или робота необходимо контролировать. Для этого в S#.API есть специальный класс LogManager. Данный класс принимает сообщения от источников и передает их в слушатели. В нашем случае источниками будут Connector, стратегии и т.д., а слушателем будет файл и панель логов.

В коде MainWindow объявляем объект LogManager и место, где он будет храниться:

public readonly LogManager LogManager;
private static readonly string _logsDir = $@"{_dir}\Logs\";


В конструкторе MainWindow создаем LogManager, задаем ему источник Connector и файл слушателя:

//----------------------------------------------------------------------------------
LogManager = new LogManager();
LogManager.Sources.Add(Connector);
LogManager.Listeners.Add(new FileLogListener
{
        SeparateByDates = SeparateByDateModes.SubDirectories,
        LogDirectory = _logsDir
});
//----------------------------------------------------------------------------------


По аналогии с панелью инструментов создадим, панель логов в папку XAML добавляем еще один UserControl. Дадим ему имя MonitorControl. В него добавим элемент Monitor.


        


В конструкторе MonitorControl зададим в LogManager еще и Monitor как слушателя:

public MonitorControl()
{
        InitializeComponent();
        MainWindow.Instance.LogManager.Listeners.Add(new GuiLogListener(Monitor));
}


В нижнюю часть MainWindow добавляем созданный MonitorControl:


        
        


Запускаем для проверки:

kyxgmwzz8wd1cnl91bhpt1007be.png

Создание панели стаканов


По аналогии с предыдущими панелями создадим панель стаканов, в папку XAML добавляем еще один UserControl. Дадим ему имя MarketDepthControl.

В MainWindow мы уже использовали LayoutControl, в этом контроле тоже воспользуемся LayoutControl. Разобьем панель на две части по горизонтали:

 
        
                
                
                
                
                
                
        


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


        

Правую часть разобьем на части по вертикали. Сверху правой части будет стакан:

        
                
        


У MarketDepthControl необходимо задать какое-нибудь значение MaxHeight, иначе приложение не будет запускаться.

Под стаканом расположим элементы задания портфеля, цены, и объёма заявки:


        


        


        


Здесь стоит отметить свойство Label у LayoutItem, оно позволяет задать текст перед элементом. А также элемент SpinEdit от DevExpress в котором удобно задавать численные значения. Выглядят эти элементы следующим образом:

grix9jsfrxcmaftyx6bpdylnxi4.png

Еще ниже расположим кнопки купить и продать:


        
                
        
        
                
        


Полный код:


        
                
                        
                
                
                        
                                
                        
                        
                                
                        
                        
                                
                        
                        
                                
                        
                        
                                
                                        
                                
                                
                                        
                                
                        
                
        


В конструкторе MarketDepthControl зададим источник инструментов для SecurityPicker и источник портфелей для PortfolioComboBox, в нашем случае это будет Connector:

public MarketDepthControl()
{
        InitializeComponent();
        SecPicker.SecurityProvider = MainWindow.Instance.Connector;
        PortfolioComboBox.Portfolios = new PortfolioDataSource(MainWindow.Instance.Connector);
}


Создадим обработчик события выделения инструмента в SecurityPicker. В нем проверяем не равен ли нулю полученный инструмент. Если он не равен нулю, сохраняем полученный инструмент в локальную переменную, нам он пригодится при обновлении стакана. После чего очищаем и регистрируем полученный инструмент в Connector на получение стакана с помощью метода RegisterMarketDepth. С помощью метода GetMarketDepth получаем текущий стакан по инструменту, чтобы им обновить MarketDepthControl.

private Security _selectedSecurity;
private void SecPicker_SecuritySelected(Security security)
{
        if (security == null) return;
        _selectedSecurity = security;
        MainWindow.Instance.Connector.RegisterMarketDepth(_selectedSecurity);
        var marketDepth = MainWindow.Instance.Connector.GetMarketDepth(_selectedSecurity);
        MarketDepth.UpdateDepth(marketDepth);
}


Чтобы стакан постоянно обновлялся в конструкторе MarketDepthControl, подпишемся на событие изменения стакана MarketDepthChanged у коннектора. В обработчике этого события будем проверять, какому инструменту принадлежит полученный стакан, и если он принадлежит выделенному инструменту в SecurityPicker, то обновляем им: MarketDepthControl.

public MarketDepthControl()
{
        InitializeComponent();
        SecPicker.SecurityProvider = MainWindow.Instance.Connector;
        PortfolioComboBox.Portfolios = new PortfolioDataSource(MainWindow.Instance.Connector);
        MainWindow.Instance.Connector.MarketDepthChanged += Connector_MarketDepthChanged;
}

private void Connector_MarketDepthChanged(MarketDepth marketDepth)
{
        if (_selectedSecurity == null || 
                marketDepth.Security != _selectedSecurity) return;
        MarketDepth.UpdateDepth(marketDepth);
}


В центральную части MainWindow добавляем созданную панель MarketDepthControl:


        
        
                
        
        
                
        
        
                
        
        
                
        
        
                
        


На данном этапе можно запустить программу и проверить работу обновления стаканов.
Создадим обработчика события нажатия на кнопки купить и продать. В каждом обработчике создаем Order, в нем указываем инструмент выбранный в SecurityPicker, портфель выбранный в PortfolioComboBox, объём и цену из соответствующих SpinEdit. Регистрируем заявку в Connector с помощью метода RegisterOrder.

private void BuyButton_Click(object sender, RoutedEventArgs e)
{
        Order order = new Order()
        {
                Security = _selectedSecurity,
                Portfolio = PortfolioComboBox.SelectedPortfolio,
                Volume = SpinEditVolume.Value,
                Price = SpinEditPrice.Value,
                Direction = StockSharp.Messages.Sides.Buy,
        };
        MainWindow.Instance.Connector.RegisterOrder(order);
}

private void SelltButton_Click(object sender, RoutedEventArgs e)
{
        Order order = new Order()
        {
                Security = _selectedSecurity,
                Portfolio = PortfolioComboBox.SelectedPortfolio,
                Volume = SpinEditVolume.Value,
                Price = SpinEditPrice.Value,
                Direction = StockSharp.Messages.Sides.Sell,
        };
        MainWindow.Instance.Connector.RegisterOrder(order);
}


Оба обработчика отличаются только направлением заявки.

Сделаем чтобы при выделении котировки в стакане значение SpinEditPrice менялось на цену выделенной котировки. Для этого создадим обработчик события SelectionChanged у MarketDepthControl. В котором будем обновлять значение SpinEditPrice ценой выделенной котировки если выделенная котировка не равна нулю.

private void MarketDepth_SelectionChanged(object sender, 
        GridSelectionChangedEventArgs e)
{
        if (MarketDepth.SelectedQuote == null)
                return;
        SpinEditPrice.Value = MarketDepth.SelectedQuote.Price;
}


Запускаем для проверки:

p915of4vabbulkuyg8zjlvxzsei.png

Сохранение маркет-данных


Для сохранения портфелей, инструментов, площадок нам необходим класс CsvEntityRegistry. В него надо переделать место хранения сущностей и вызвать метод Init, для их загрузки.

  _csvEntityRegistry = new CsvEntityRegistry(_csvEntityRegistryDir);
        _csvEntityRegistry.Init();


Для сохранения свечей, сделок и т.д. нам понадобится StorageRegistry:

  _storageRegistry = new StorageRegistry
        {
                DefaultDrive = new LocalMarketDataDrive(_storageRegistryDir),
        };


Также нам понадобится реестр хранилищ-снэпшотов SnapshotRegistry:

_snapshotRegistry = new SnapshotRegistry(_snapshotRegistryDir);


Все это мы передаем в Connector при его создании:

Connector = new Connector(_csvEntityRegistry, _storageRegistry, _snapshotRegistry)
{
        IsRestoreSubscriptionOnReconnect = true,
        StorageAdapter = { DaysLoad = TimeSpan.FromDays(3) },
};
Connector.LookupAll();


Здесь мы также указали, что Connector будет переподключаться при разрыве подключения, а также указали сколько дней истории необходимо загружать. Строка Connector.LookupAll (); запрашивает имеющиеся данные:

//----------------------------------------------------------------------------------
Directory.CreateDirectory(_dir);

_csvEntityRegistry = new CsvEntityRegistry(_csvEntityRegistryDir);
_csvEntityRegistry.Init();
_storageRegistry = new StorageRegistry
{
        DefaultDrive = new LocalMarketDataDrive(_storageRegistryDir),
};
_snapshotRegistry = new SnapshotRegistry(_snapshotRegistryDir);
Connector = new Connector(_csvEntityRegistry, _storageRegistry, _snapshotRegistry)
        {
                IsRestoreSubscriptionOnReconnect = true,
                StorageAdapter = { DaysLoad = TimeSpan.FromDays(3) },
        };
Connector.LookupAll();

if (File.Exists(_settingsFile))
{
                Connector.Load(new XmlSerializer().Deserialize(_settingsFile));
}
//----------------------------------------------------------------------------------


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

ur5ajx_3klmkbkcfzkyrqqamuza.png

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

Мы плавно подошли к окончанию первой части. На данном этапе программа позволяет выводить на экран все доступные нам маркет-данные. В следующей части будет продемонстрирована самое лакомое —, а именно торговля как в ручном, так и в автоматическом режиме.

Продолжение следует…

Автор: Иван Залуцкий

© Habrahabr.ru