В данной статье я покажу как использовать графические компоненты, входящий в S#.API, с целью создания полноценного приложения уровня
S#.Shell:
Вы узнаете, как сделать профессионального уровня программу с настройкой подключений, выводом инструментов, цен и графиков (и чтобы это все еще сохранялось и загружалось при перезапуске). И сложность создания такого приложения не несколько месяцев, а буквально несколько часов (это не шутка! читайте до конца). В этом заключается основное преимущество графического фреймворка, который я назвал по аналогии S#.UI (данное название не официальное, я сам придумал).
Я не буду использовать сложные конструкции и паттерны проектирования, понятные только профессиональным программистам. Наоборот, цель статьи показать, что порог вхождения в создание своих приложений торговли с помощью S#.API очень низкий.
Если вы работаете в компании, и делаете свой уникальный софт (например, вы работает в проп или брокерской компании), вам так же будет интересно. В этой статье вы сможете узнать практику создания подобных систем (особенно, если вы только приступили к своим обязанностям).
Что понадобиться
1) Visual Studio 2017 (Community, бесплатная версия), в ней мы будем программировать.
2) Бесплатное подключение к тестовым торгам на бирже, я буду использовать QUIK.
Создание проекта
Создадим новое WPF приложение в Visual Studio
После чего необходимо добавить S#.API библиотеки в как это сделать описано
здесь . Я предпочитаю установку при помощи Nuget.
Так как все графические элементы S#.API созданы на базе DevExpress, а библиотеки DevExpress идут вместе с S#.API, глупо будет ими не воспользоваться. Всю информацию по графическим элементам DevExpress можно найти в Google.
Перейдем в редактор окна MainWindow.xaml
Заменим Window на DXWindow, это нам понадобиться для использования разных цветовых схем
Visual Studio нам сама предложит вставить необходимые библиотеки.
Разобьем окно на три части в верху будет полоса с кнопками настройки подключений и подключения. В низу окно с логами. В середине все остальные панели. Проще всего так разбить окно с помощью LayoutControl от DevExpress.
В получившиеся три части мы и будем добавлять необходимые нам элементы.
Настройка подключения к коннектору
Добавим две кнопки, одна кнопка настройки подключения, а вторая кнопка подключения. Для этого воспользуемся кнопкой SimpleButton от DevExpress. Кнопки будут расположены в верхней части приложения. В каждую кнопку поместим картинку привычные по Терминалу и Дизайнеру.
В верхнем правом углу экранной экранной формы увидим такую картину
Двойным нажатием на каждую кнопку создадим обработчики событий нажатия на кнопку.
В коде MainWindow необходимо объявить коннектор, а также место и имя файла в котором будут храниться настройки коннектора.
В обработчике события нажатия на кнопку настроек коннектора будем открывать окно конфигурации коннектора и сохранять его в файл.
В конструкторе будем проверять есть ли каталог и файл с настройками коннектора и если он есть будем его загружать в коннектор
Большинство объектов S#.API имеют методы Save и Load, с помощью которых можно сохранить и загрузить этот объект из XML файла.
В методе обработчике нажатия на кнопку подключения подключаем коннектор.
Теперь можно запустить программу и проверить ее.
Установка темной темы
Я предпочитаю темную тему. Поэтому сразу делаем чтобы тема программы была темной. Для этого в файле App.xaml
Заменяем Application на charting:ExtendedBaseApplication Visual Studio нам сама предложит вставить необходимые библиотеки. А в файле App.xaml.cs удалить «: Application». Получиться код следующего вида
В конструкторе MainWindow пишем ApplicationThemeHelper.ApplicationThemeName = Theme.VS2017DarkName;
Полный код на текущий момент:
Запускаем для проверки темной темы.
Создание панели инструментов
Добавим папку, где мы будем хранить все созданные нами контроллы и назовем ее XAML.
Добавим в нее свой первый UserControll, дадим ему имя SecurityGridControl .
В него добавляем один элемент SecurityPicker. В котором будут отображаться имеющиеся инструменты. По аналогии с главным окном будем использовать LayoutControl от DevExpress.
Перейдем в конструктор главного окна и изменим центральную часть в вид закладок. В одной из закладок расположим созданный нами контролл с SecurityPicker
Теперь, когда у нас есть панель инструментов надо задать ей источник данных, в нашем случае это коннектор. Можно было просто в конструкторе MainWindow написать
SecurityPanel.SecPicker.SecurityProvider = Connector;
Но не стоит засорять код MainWindow кодом, который к нему не относится. Поэтому я создам статическую переменную Instance а в конструкторе MainWindow присвою ему значение MainWindow.
Теперь в любом месте нашей программы мы можем обращаться к свойствам MainWindow через код MainWindow.Instance.XXX.
В конструкторе SecurityGridControl таким образом указываем Connector как источник данных
Запустим для проверки.
Добавление логирования
Работу программы, коннектора или робота необходимо контролировать. Для этого в S#.API есть специальный класс LogManager. Данный класс принимает сообщения от источников и передает их в слушатели. В нашем случае источниками будут Connector, стратегии и т.д., а слушателем будет файл и панель логов.
В коде MainWindow объявляем объект LogManager и место, где он будет храниться
В конструкторе MainWindow создаем LogManager, задаем ему источник Connector и задаем слушателя файл
По аналогии с панелью инструментов создадим, панель логов в папку XAML добавляем еще один UserControl. Дадим ему имя MonitorControl. В него добавим элемент Monitor.
В конструкторе MonitorControl зададим в LogManager еще и Monitor как слушателя
В нижнюю часть MainWindow добавляем созданный MonitorControl
Запускаем для проверки
Создание панели портфелей
По аналогии с панелью инструментов создадим, панель логов в папку XAML добавляем еще один UserControl. Дадим ему имя PortfolioGridControl. В него добавим элемент PortfolioGrid.
В конструкторе PortfolioGridControl нам надо подписаться на события появления нового портфеля и событие появления новой позиции у Connector.
Таким образом при появлении нового портфеля он появиться на панели портфелей портфель, а при появлении новой позиции на панели портфелей портфель обновит позицию.
В центральную части MainWindow добавляем созданную панель PortfolioGridControl
Запускаем для проверки
У нас появилась вкладка с портфелями.
Создание панели ордеров
Панель ордеров в S#.API имеет возможность выставления заявок, снятия заявок и перерегистрации.
По аналогии с панелью инструментов создадим панель ордеров, в папку XAML добавляем еще один UserControl. Дадим ему имя OrderGridControl. В него добавим элемент OrderGrid.
OrderGrid имеет событие регистрации заявки OrderRegistering, событие перерегистрации заявки OrderReRegistering и событие отмены заявки OrderCanceling.
Создадим их обработчики
В обработчике события регистрации заявки мы создаем окно OrderWindow, в котором необходимо указать источники данных для инструментов, портфелей, и рыночных данных. В нашем случае это все будет Connector.
После чего мы вызываем OrderWindow методом ShowModal если в этом окне было нажата кнопка ОК то мы через коннектор методом RegisterOrder регистрируем заявку.
В обработчике события перерегистрации заявки мы все делаем тоже самое. Только в этом случае в событие нам приходит объект Order это заявка, которую надо перерегистрировать. Поэтому в OrderWindow мы указываем Order = order.ReRegisterClone(newVolume: order.Balance), чтобы заполнить поля окна OrderWindow.
После чего мы вызываем OrderWindow методом ShowModal если в этом окне было нажата кнопка ОК то мы через коннектор методом ReRegisterClone перерегистрируем заявку. В него мы передаем старую заявку, которую надо отменить и новую которую надо выставить.
В обработчике события отмены заявки достаточно вызвать метод CancelOrder и передать в него ордер, который необходимо отменить.
Чтобы Ордера отображались в OrderGrid необходимо в конструкторе OrderGridControl подписаться на события появления нового ордера и на событие ошибки регистрации и передавать эти события в OrderGrid.
В центральную части MainWindow добавляем созданную панель OrderGridControl
Запускаем для проверки
Создание панели собственных сделок
По аналогии с панелью инструментов создадим панель собственных сделок, в папку XAML добавляем еще один UserControl. Дадим ему имя MyTradeGridControl. В него добавим элемент MyTradeGrid.
В конструкторе MyTradeGridControl нам надо подписаться на события появления новой собственной сделки и передать ее в MyTradeGrid.
В центральную части MainWindow добавляем созданную панель OrderGridControl
Запускаем для проверки
Создание панели стаканов
По аналогии с предыдущими панелями создадим панель стаканов, в папку XAML добавляем еще один UserControl. Дадим ему имя MarketDepthControl.
В MainWindow мы уже использовали LayoutControl, в этом контроле тоже воспользуемся LayoutControl. Разобьем панель на две части по горизонтали
В левую часть добавим SecurityPicker с ним мы встречались, когда создавали панель инструментов.
Правую часть разобьем на части по вертикали. Сверху правой части будет стакан
У MarketDepthControl необходимо задать какое-нибудь значение MaxHeight иначе приложение не будет запускаться.
Под стаканом расположим элементы задания портфеля, цены, и объёма заявки
Здесь стоит отметить свойство Label у LayoutItem, оно позволяет заладь текст перед элементом. А также элемент SpinEdit от DevExpress в котором удобно задавать численные значения. Выглядят эти элементы следующим образом.
Еще ниже расположим кнопки купить, продать.
Полный код
В конструкторе MarketDepthControl зададим источник инструментов для SecurityPicker и источник портфелей для PortfolioComboBox, в нашем случае это будет Connector.
Создадим обработчик события выделения инструмента в SecurityPicker. В нем проверяем не равен ли нулю полученный инструмент. Если он не равен нулю сохраняем полученный инструмент в локальную переменную, нам он пригодиться при обновлении стакана. После чего очищаем регистрируем полученный инструмент в Connector на получение стакана с помощью метода RegisterMarketDepth. С помощь метода GetMarketDepth получаем текущий стакана по инструменту, чтобы им обновить MarketDepthControl.
Чтобы стакан постоянно обновлялся в конструкторе MarketDepthControl подпишемся на событие изменения стакана MarketDepthChanged у коннектора. В обработчике этого события будем проверять какому инструменту принадлежит полученный стакан, и если он принадлежит выделенному инструменту в SecurityPicker то обновляем им MarketDepthControl.
В центральную части MainWindow добавляем созданную панель MarketDepthControl
На данном этапе можно запустить программу и проверить работу обновления стаканов.
Создадим обработчика события нажатия на кнопки купить и продать. В каждом обработчике создаем Order, в нем указываем инструмент выбранный в SecurityPicker, портфель выбранный в PortfolioComboBox, объём и цену из соответствующих SpinEdit. Регистрируем заявку в Connector с помощью метода RegisterOrder.
Оба обработчика отличаются только направлением заявки.
Сделаем чтобы при выделении котировки в стакане значение SpinEditPrice менялось на цену выделенной котировки. Для этого создадим обработчик события SelectionChanged у MarketDepthControl. В котором будем обновлять значение SpinEditPrice ценой выделенной котировки если выделенная котировка не равна нулю.
Запускаем для проверки
Сохранение маркет-данных
Для сохранения портфелей, инструментов, площадок нам необходим класс CsvEntityRegistry. В него надо переделать место хранения сущностей и вызвать метод Init, для их загрузки.
Для сохранения свечей, сделок и т.д. нам понадобиться StorageRegistry
Также нам понадобиться реестр хранилищ-снэпшотов SnapshotRegistry
Все это мы передаем в Connector при его создании
Здесь я также указал что Connector будет переподключаться при разрыве подключения, а также указал сколько дней истории необходимо загружать.
Строка Connector.LookupAll(); запрашивает имеющиеся данные.
После загрузки приложения перейдя в папку Data мы увидим, что появились новые папки.
А при повторном подключении панели инструментов и портфелей уже будут заполнены.
Создание панели со стратегией
Панель стратегий я буду создавать также, как и все предыдущие панели.
В папку XAML добавляем еще один UserControl. Дадим ему имя StrategyControl. С помощь LayoutControl разобьём экранную форму на две части.
В левой части будут вкладка с свечным графиком
А также вкладка статистикой стратегии,
Здесь я использую StatisticParameterGrid для отображения статистики стратегии, а также EquityCurveChart для отображения графика прибыли и убытка.
У StatisticParameterGrid необходимо задать какое-нибудь значение MaxHeight иначе приложение не будет запускаться.
В правой части будет проводиться настройка свойств стратегии в PropertyGridEx
А также кнопки запуска и остановки стратегии.
Полный код
В конструкторе StrategyControl задаем Connector как источники данных для PropertyGridEx, почти в каждом контроле мы выполняли подобные действия.
Нам необходимо как-то передать стратегию в наш контрол. Для этого в StrategyControl создам метод BindStraregy в который будет принимать стратегию, сохранять ссылку на нее в локальной переменной, а также задавать стратегию в PropertyGridEx и StatisticParameterGrid.
С помощь метода SetChart в стратегию предаём график свечей Chart, после этого в стратегии Chart можно будет получить с помощью метода GetChart. Также задаем Connector для стратегии.
При работе с графиком прибыли и убытков надо учесть, что стратегия будем запускать и останавливать и возможно несколько раз, поэму с каждым запуском стратегии график надо очищать. Для это создадим метод ResetEquityCurveChart в котором будем сначала очищать EquityCurveChart. После чего нам необходимо создать графические элементы для EquityCurveChart, им можно указать имя, цвет и тип линии.
После чего подпишемся на событие изменения PnL у стратегии и в обработчике этого события отрисовываем новое значение на графике прибыли убытков EquityCurveChart.
Полный код метода
В обработчике события нажатия на кнопку Старт будем вызвать этот метод. А также будем сбрасывать состояние стратегии и запускать ее.
В обработчике события нажатия на кнопку Стоп будем останавливать стратегию.
В центральную части MainWindow добавляем созданную панель StrategyControl
Создание стратегии
Для примера рассмотрим создание простой стратегии со свечами. Которая будет покупать если свеча растущая (зеленая) и продавать если свеча убывающая (красная).
Создадим еще одну папку в проекте в ней будем хранить все наши стратегии. В этой папке создаем новый класс и назовем его SimpleStrategy. Все стратегии S# должны наследоваться от базового класса стратегии Strategy.
Так как наша стратегия использует свечи то создадим публичное свойство CandleSeries а в конструкторе нашей стратегии зададим ему значение по умолчанию.
Здесь я указал что свечи в CandleSeries будут TimeFrameCandle, с интервалом 15 секунд (TimeSpan.FromSeconds(15)). Для CandleSeries можно указать режим создания свечей BuildCandlesMode. Я указал что свечи будут построены (MarketDataBuildModes.Build), по умолчанию они будут строиться из тиков, но можно указать и другие типы данных.
Так как CandleSeries мы сделали публичным свойством, то CandleSeries можно будет дополнительно настроить из PropertyGridEx описанном в предыдущем пункте.
Все стратегии имеют методы который можно переопределить, нам понадобиться переопределить метод OnStarted. Который вызывается перед запуском стратегии и позволяет предварительно задать ей стартовое состояние.
Здесь мы для CandleSeries задаем инструмент, который указывается в PropertyGridEx. После чего создаем правило обработки законченной свечи. О работе с правилами можно ознакомиться в документации. В правиле указываем метод, который будет обрабатывать каждую законченную свечу в нашем случае это метод ProcessCandle он будет описан позже. После того как все задано подписываемся на появление свечей по CandleSeries в коннекторе через метод SubscribeCandles.
В нашем случае метод ProcessCandle и содержит основную логику стратегии.
В первую очередь нам необходимо определить является ли свеча реал тайм или исторической, если свеча историческая, то мы ее игнорируем. Не все стратегии требуют этого, например для стратегий основанные на стаканах не требуют этого так как стаканы идут всегда реал тайм. Нет универсального способа определить является ли свеча реал тайм или исторической, и в каждой стратегии эту проблему придется решать самостоятельно в зависимости от требований стратегии. В данном случае я просто буду сравнивать время закрытие свечи с временем в коннекторе и если оно не превышает определенный лаг, то свечу считаю реал тайм.
Далее смотрим на то какая это свеча и какая текущая позиция у стратегии. Если свеча растущая, то при позиции равной 0 мы откроем позицию рыночным ордером на объём, заданный нами в PropertyGridEx. Если свеча растущая и позиция меньше 0 то мы переворачиваем позицию.
Противоположные действия делаем для убывающей свечи.
На данный момент наша стратегия готова к работе. Ее необходимо передать в SimpleStrategyControl который мы создали в предыдущем пункте с помощью метода BindStraregy. Это мы делаем в конструкторе MainWindow сразу после инициализации компонентов MainWindow.
Запустим для проверки.
Стратегия работает, совершаются сделки, но пока нет свечей и сделок на графике.
Добавление свечей и сделок на график из стратегии
В пункте про панель стратегий с помощь метода SetChart в стратегию мы предали график свечей Chart. В методе OnStarted стратегии проверяем установлен ли График у стратегии и если он установлен, то инициализируем график, а также подписываемся на события появления новой собственной сделки и изменения свечи.
Метод инициализации графика InitChart.
Здесь мы сохраняем ссылку на Сhart в локальной переменной. Очищаем график. А также создаем и передаем на график элементы графика для свечей и сделок.
Конструкция _chart.GuiSync(() =>{ ... }); нужна для того чтобы инициализация графика выполнилась в главном потоке.
Метод отрисовки свечей на графике CandleSeriesProcessing.
Здесь мы получаем свеча из события CandleSeriesProcessing коннектора, создаем ChartDrawData для отображения его на графике. Указываем время data.Group(candle.OpenTime), указываем что свечу надо добавить в свечной элемент графика .Add(_chartCandleElement, candle);. И указываем что графику надо прорисовать новые данные.
Аналогичные действия выполняем для сделок.
Запустим для проверки.
Краткий вывод
Для создание сложного и профессионально выглядящего приложения не нужно тратить массу времени. Мы за несколько часов создали полноценное приложение с возможностью конфигурирование, отображения и непосредственной торговли.
Не бойтесь пробовать и создавать свои программы. Надеюсь, эта статья вам поможет освоиться в этом деле.