S#.UI – графический фреймворк StockSharp

S#.UI – графический фреймворк StockSharp
Atom
11/13/2018
Иван З.


В данной статье я покажу как использовать графические компоненты, входящий в S#.API, с целью создания полноценного приложения уровня S#.Shell:
image3979.png
Вы узнаете, как сделать профессионального уровня программу с настройкой подключений, выводом инструментов, цен и графиков (и чтобы это все еще сохранялось и загружалось при перезапуске). И сложность создания такого приложения не несколько месяцев, а буквально несколько часов (это не шутка! читайте до конца). В этом заключается основное преимущество графического фреймворка, который я назвал по аналогии S#.UI (данное название не официальное, я сам придумал).
Я не буду использовать сложные конструкции и паттерны проектирования, понятные только профессиональным программистам. Наоборот, цель статьи показать, что порог вхождения в создание своих приложений торговли с помощью S#.API очень низкий.
Если вы работаете в компании, и делаете свой уникальный софт (например, вы работает в проп или брокерской компании), вам так же будет интересно. В этой статье вы сможете узнать практику создания подобных систем (особенно, если вы только приступили к своим обязанностям).

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


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

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


Создадим новое WPF приложение в Visual Studio
image5545.png
После чего необходимо добавить S#.API библиотеки в как это сделать описано здесь . Я предпочитаю установку при помощи Nuget.
Так как все графические элементы S#.API созданы на базе DevExpress, а библиотеки DevExpress идут вместе с S#.API, глупо будет ими не воспользоваться. Всю информацию по графическим элементам DevExpress можно найти в Google.
Перейдем в редактор окна MainWindow.xaml
image4259.png
Заменим Window на DXWindow, это нам понадобиться для использования разных цветовых схем
image4329.png
Visual Studio нам сама предложит вставить необходимые библиотеки.
Разобьем окно на три части в верху будет полоса с кнопками настройки подключений и подключения. В низу окно с логами. В середине все остальные панели. Проще всего так разбить окно с помощью LayoutControl от DevExpress.
В получившиеся три части мы и будем добавлять необходимые нам элементы.
image2275.png

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


Добавим две кнопки, одна кнопка настройки подключения, а вторая кнопка подключения. Для этого воспользуемся кнопкой SimpleButton от DevExpress. Кнопки будут расположены в верхней части приложения. В каждую кнопку поместим картинку привычные по Терминалу и Дизайнеру.
image9977.png
В верхнем правом углу экранной экранной формы увидим такую картину
image1157.png
Двойным нажатием на каждую кнопку создадим обработчики событий нажатия на кнопку.
В коде MainWindow необходимо объявить коннектор, а также место и имя файла в котором будут храниться настройки коннектора.
image836.png
В обработчике события нажатия на кнопку настроек коннектора будем открывать окно конфигурации коннектора и сохранять его в файл.
image1289.png
В конструкторе будем проверять есть ли каталог и файл с настройками коннектора и если он есть будем его загружать в коннектор
image3497.png
Большинство объектов S#.API имеют методы Save и Load, с помощью которых можно сохранить и загрузить этот объект из XML файла.
В методе обработчике нажатия на кнопку подключения подключаем коннектор.
image6463.png
Теперь можно запустить программу и проверить ее.

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


Я предпочитаю темную тему. Поэтому сразу делаем чтобы тема программы была темной. Для этого в файле App.xaml
image1012.png
Заменяем Application на charting:ExtendedBaseApplication Visual Studio нам сама предложит вставить необходимые библиотеки. А в файле App.xaml.cs удалить «: Application». Получиться код следующего вида
image4777.png
image9701.png
В конструкторе MainWindow пишем ApplicationThemeHelper.ApplicationThemeName = Theme.VS2017DarkName;
Полный код на текущий момент:
image8484.png
Запускаем для проверки темной темы.
image1199.png

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


Добавим папку, где мы будем хранить все созданные нами контроллы и назовем ее XAML.
Добавим в нее свой первый UserControll, дадим ему имя SecurityGridControl .
image5159.png
В него добавляем один элемент SecurityPicker. В котором будут отображаться имеющиеся инструменты. По аналогии с главным окном будем использовать LayoutControl от DevExpress.
image2385.png
Перейдем в конструктор главного окна и изменим центральную часть в вид закладок. В одной из закладок расположим созданный нами контролл с SecurityPicker
image5375.png
Теперь, когда у нас есть панель инструментов надо задать ей источник данных, в нашем случае это коннектор. Можно было просто в конструкторе MainWindow написать
SecurityPanel.SecPicker.SecurityProvider = Connector;
Но не стоит засорять код MainWindow кодом, который к нему не относится. Поэтому я создам статическую переменную Instance а в конструкторе MainWindow присвою ему значение MainWindow.
image1508.png
Теперь в любом месте нашей программы мы можем обращаться к свойствам MainWindow через код MainWindow.Instance.XXX.
В конструкторе SecurityGridControl таким образом указываем Connector как источник данных
image6003.png
Запустим для проверки.
image8483.png

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


Работу программы, коннектора или робота необходимо контролировать. Для этого в S#.API есть специальный класс LogManager. Данный класс принимает сообщения от источников и передает их в слушатели. В нашем случае источниками будут Connector, стратегии и т.д., а слушателем будет файл и панель логов.
В коде MainWindow объявляем объект LogManager и место, где он будет храниться
image3414.png
В конструкторе MainWindow создаем LogManager, задаем ему источник Connector и задаем слушателя файл
image5931.png
По аналогии с панелью инструментов создадим, панель логов в папку XAML добавляем еще один UserControl. Дадим ему имя MonitorControl. В него добавим элемент Monitor.
image4717.png
В конструкторе MonitorControl зададим в LogManager еще и Monitor как слушателя
image8141.png
В нижнюю часть MainWindow добавляем созданный MonitorControl
image5572.png
Запускаем для проверки
image6299.png

Создание панели портфелей


По аналогии с панелью инструментов создадим, панель логов в папку XAML добавляем еще один UserControl. Дадим ему имя PortfolioGridControl. В него добавим элемент PortfolioGrid.
image68.png
В конструкторе PortfolioGridControl нам надо подписаться на события появления нового портфеля и событие появления новой позиции у Connector.
image1333.png
Таким образом при появлении нового портфеля он появиться на панели портфелей портфель, а при появлении новой позиции на панели портфелей портфель обновит позицию.
В центральную части MainWindow добавляем созданную панель PortfolioGridControl
image127.png
Запускаем для проверки
image3862.png
У нас появилась вкладка с портфелями.

Создание панели ордеров


Панель ордеров в S#.API имеет возможность выставления заявок, снятия заявок и перерегистрации.
По аналогии с панелью инструментов создадим панель ордеров, в папку XAML добавляем еще один UserControl. Дадим ему имя OrderGridControl. В него добавим элемент OrderGrid.
image4502.png
OrderGrid имеет событие регистрации заявки OrderRegistering, событие перерегистрации заявки OrderReRegistering и событие отмены заявки OrderCanceling.
Создадим их обработчики
image6128.png
В обработчике события регистрации заявки мы создаем окно OrderWindow, в котором необходимо указать источники данных для инструментов, портфелей, и рыночных данных. В нашем случае это все будет Connector.
После чего мы вызываем OrderWindow методом ShowModal если в этом окне было нажата кнопка ОК то мы через коннектор методом RegisterOrder регистрируем заявку.
image4848.png
В обработчике события перерегистрации заявки мы все делаем тоже самое. Только в этом случае в событие нам приходит объект Order это заявка, которую надо перерегистрировать. Поэтому в OrderWindow мы указываем Order = order.ReRegisterClone(newVolume: order.Balance), чтобы заполнить поля окна OrderWindow.
После чего мы вызываем OrderWindow методом ShowModal если в этом окне было нажата кнопка ОК то мы через коннектор методом ReRegisterClone перерегистрируем заявку. В него мы передаем старую заявку, которую надо отменить и новую которую надо выставить.
image1314.png
В обработчике события отмены заявки достаточно вызвать метод CancelOrder и передать в него ордер, который необходимо отменить.
image2355.png
Чтобы Ордера отображались в OrderGrid необходимо в конструкторе OrderGridControl подписаться на события появления нового ордера и на событие ошибки регистрации и передавать эти события в OrderGrid.
image9790.png
В центральную части MainWindow добавляем созданную панель OrderGridControl
image4806.png
Запускаем для проверки
image7488.png

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


По аналогии с панелью инструментов создадим панель собственных сделок, в папку XAML добавляем еще один UserControl. Дадим ему имя MyTradeGridControl. В него добавим элемент MyTradeGrid.
image3653.png
В конструкторе MyTradeGridControl нам надо подписаться на события появления новой собственной сделки и передать ее в MyTradeGrid.
image7227.png
В центральную части MainWindow добавляем созданную панель OrderGridControl
image3738.png
Запускаем для проверки
image514.png

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


По аналогии с предыдущими панелями создадим панель стаканов, в папку XAML добавляем еще один UserControl. Дадим ему имя MarketDepthControl.
В MainWindow мы уже использовали LayoutControl, в этом контроле тоже воспользуемся LayoutControl. Разобьем панель на две части по горизонтали
image1678.png
В левую часть добавим SecurityPicker с ним мы встречались, когда создавали панель инструментов.
image7627.png
Правую часть разобьем на части по вертикали. Сверху правой части будет стакан
image1159.png
У MarketDepthControl необходимо задать какое-нибудь значение MaxHeight иначе приложение не будет запускаться.
Под стаканом расположим элементы задания портфеля, цены, и объёма заявки
image8282.png
Здесь стоит отметить свойство Label у LayoutItem, оно позволяет заладь текст перед элементом. А также элемент SpinEdit от DevExpress в котором удобно задавать численные значения. Выглядят эти элементы следующим образом.
image4967.png
Еще ниже расположим кнопки купить, продать.
image62.png
Полный код
image9810.png
В конструкторе MarketDepthControl зададим источник инструментов для SecurityPicker и источник портфелей для PortfolioComboBox, в нашем случае это будет Connector.
image651.png
Создадим обработчик события выделения инструмента в SecurityPicker. В нем проверяем не равен ли нулю полученный инструмент. Если он не равен нулю сохраняем полученный инструмент в локальную переменную, нам он пригодиться при обновлении стакана. После чего очищаем регистрируем полученный инструмент в Connector на получение стакана с помощью метода RegisterMarketDepth. С помощь метода GetMarketDepth получаем текущий стакана по инструменту, чтобы им обновить MarketDepthControl.
image3667.png
Чтобы стакан постоянно обновлялся в конструкторе MarketDepthControl подпишемся на событие изменения стакана MarketDepthChanged у коннектора. В обработчике этого события будем проверять какому инструменту принадлежит полученный стакан, и если он принадлежит выделенному инструменту в SecurityPicker то обновляем им MarketDepthControl.
image4084.png
В центральную части MainWindow добавляем созданную панель MarketDepthControl
image7347.png
На данном этапе можно запустить программу и проверить работу обновления стаканов.
Создадим обработчика события нажатия на кнопки купить и продать. В каждом обработчике создаем Order, в нем указываем инструмент выбранный в SecurityPicker, портфель выбранный в PortfolioComboBox, объём и цену из соответствующих SpinEdit. Регистрируем заявку в Connector с помощью метода RegisterOrder.
image7717.png
Оба обработчика отличаются только направлением заявки.
Сделаем чтобы при выделении котировки в стакане значение SpinEditPrice менялось на цену выделенной котировки. Для этого создадим обработчик события SelectionChanged у MarketDepthControl. В котором будем обновлять значение SpinEditPrice ценой выделенной котировки если выделенная котировка не равна нулю.
image90.png
Запускаем для проверки
image8902.png

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


Для сохранения портфелей, инструментов, площадок нам необходим класс CsvEntityRegistry. В него надо переделать место хранения сущностей и вызвать метод Init, для их загрузки.
image7322.png
Для сохранения свечей, сделок и т.д. нам понадобиться StorageRegistry
image809.png
Также нам понадобиться реестр хранилищ-снэпшотов SnapshotRegistry
image4003.png
Все это мы передаем в Connector при его создании
image6704.png
Здесь я также указал что Connector будет переподключаться при разрыве подключения, а также указал сколько дней истории необходимо загружать.
Строка Connector.LookupAll(); запрашивает имеющиеся данные.
image6016.png
После загрузки приложения перейдя в папку Data мы увидим, что появились новые папки.
image7577.png
А при повторном подключении панели инструментов и портфелей уже будут заполнены.

Создание панели со стратегией


Панель стратегий я буду создавать также, как и все предыдущие панели.
В папку XAML добавляем еще один UserControl. Дадим ему имя StrategyControl. С помощь LayoutControl разобьём экранную форму на две части.
В левой части будут вкладка с свечным графиком
image344.png
А также вкладка статистикой стратегии,
image4907.png
Здесь я использую StatisticParameterGrid для отображения статистики стратегии, а также EquityCurveChart для отображения графика прибыли и убытка.
У StatisticParameterGrid необходимо задать какое-нибудь значение MaxHeight иначе приложение не будет запускаться.
В правой части будет проводиться настройка свойств стратегии в PropertyGridEx
image721.png
А также кнопки запуска и остановки стратегии.
image1047.png
Полный код
image2510.png
В конструкторе StrategyControl задаем Connector как источники данных для PropertyGridEx, почти в каждом контроле мы выполняли подобные действия.
image8773.png
Нам необходимо как-то передать стратегию в наш контрол. Для этого в StrategyControl создам метод BindStraregy в который будет принимать стратегию, сохранять ссылку на нее в локальной переменной, а также задавать стратегию в PropertyGridEx и StatisticParameterGrid.
С помощь метода SetChart в стратегию предаём график свечей Chart, после этого в стратегии Chart можно будет получить с помощью метода GetChart. Также задаем Connector для стратегии.
image8230.png
При работе с графиком прибыли и убытков надо учесть, что стратегия будем запускать и останавливать и возможно несколько раз, поэму с каждым запуском стратегии график надо очищать. Для это создадим метод ResetEquityCurveChart в котором будем сначала очищать EquityCurveChart. После чего нам необходимо создать графические элементы для EquityCurveChart, им можно указать имя, цвет и тип линии.
image6360.png
После чего подпишемся на событие изменения PnL у стратегии и в обработчике этого события отрисовываем новое значение на графике прибыли убытков EquityCurveChart.
image3106.png
Полный код метода
image2024.png
В обработчике события нажатия на кнопку Старт будем вызвать этот метод. А также будем сбрасывать состояние стратегии и запускать ее.
image3375.png
В обработчике события нажатия на кнопку Стоп будем останавливать стратегию.
image3399.png
В центральную части MainWindow добавляем созданную панель StrategyControl
image8678.png

Создание стратегии


Для примера рассмотрим создание простой стратегии со свечами. Которая будет покупать если свеча растущая (зеленая) и продавать если свеча убывающая (красная).
Создадим еще одну папку в проекте в ней будем хранить все наши стратегии. В этой папке создаем новый класс и назовем его SimpleStrategy. Все стратегии S# должны наследоваться от базового класса стратегии Strategy.
image1577.png
Так как наша стратегия использует свечи то создадим публичное свойство CandleSeries а в конструкторе нашей стратегии зададим ему значение по умолчанию.
image442.png
Здесь я указал что свечи в CandleSeries будут TimeFrameCandle, с интервалом 15 секунд (TimeSpan.FromSeconds(15)). Для CandleSeries можно указать режим создания свечей BuildCandlesMode. Я указал что свечи будут построены (MarketDataBuildModes.Build), по умолчанию они будут строиться из тиков, но можно указать и другие типы данных.
Так как CandleSeries мы сделали публичным свойством, то CandleSeries можно будет дополнительно настроить из PropertyGridEx описанном в предыдущем пункте.
Все стратегии имеют методы который можно переопределить, нам понадобиться переопределить метод OnStarted. Который вызывается перед запуском стратегии и позволяет предварительно задать ей стартовое состояние.
image6026.png
Здесь мы для CandleSeries задаем инструмент, который указывается в PropertyGridEx. После чего создаем правило обработки законченной свечи. О работе с правилами можно ознакомиться в документации. В правиле указываем метод, который будет обрабатывать каждую законченную свечу в нашем случае это метод ProcessCandle он будет описан позже. После того как все задано подписываемся на появление свечей по CandleSeries в коннекторе через метод SubscribeCandles.
В нашем случае метод ProcessCandle и содержит основную логику стратегии.
image3324.png
В первую очередь нам необходимо определить является ли свеча реал тайм или исторической, если свеча историческая, то мы ее игнорируем. Не все стратегии требуют этого, например для стратегий основанные на стаканах не требуют этого так как стаканы идут всегда реал тайм. Нет универсального способа определить является ли свеча реал тайм или исторической, и в каждой стратегии эту проблему придется решать самостоятельно в зависимости от требований стратегии. В данном случае я просто буду сравнивать время закрытие свечи с временем в коннекторе и если оно не превышает определенный лаг, то свечу считаю реал тайм.
image2732.png
Далее смотрим на то какая это свеча и какая текущая позиция у стратегии. Если свеча растущая, то при позиции равной 0 мы откроем позицию рыночным ордером на объём, заданный нами в PropertyGridEx. Если свеча растущая и позиция меньше 0 то мы переворачиваем позицию.
image4474.png
Противоположные действия делаем для убывающей свечи.
image5127.png
На данный момент наша стратегия готова к работе. Ее необходимо передать в SimpleStrategyControl который мы создали в предыдущем пункте с помощью метода BindStraregy. Это мы делаем в конструкторе MainWindow сразу после инициализации компонентов MainWindow.
image374.png
Запустим для проверки.
image9399.png
image5010.png
Стратегия работает, совершаются сделки, но пока нет свечей и сделок на графике.

Добавление свечей и сделок на график из стратегии


В пункте про панель стратегий с помощь метода SetChart в стратегию мы предали график свечей Chart. В методе OnStarted стратегии проверяем установлен ли График у стратегии и если он установлен, то инициализируем график, а также подписываемся на события появления новой собственной сделки и изменения свечи.
image5547.png
Метод инициализации графика InitChart.
image977.png
Здесь мы сохраняем ссылку на Сhart в локальной переменной. Очищаем график. А также создаем и передаем на график элементы графика для свечей и сделок.
Конструкция _chart.GuiSync(() =>{ ... }); нужна для того чтобы инициализация графика выполнилась в главном потоке.
Метод отрисовки свечей на графике CandleSeriesProcessing.
image9921.png
Здесь мы получаем свеча из события CandleSeriesProcessing коннектора, создаем ChartDrawData для отображения его на графике. Указываем время data.Group(candle.OpenTime), указываем что свечу надо добавить в свечной элемент графика .Add(_chartCandleElement, candle);. И указываем что графику надо прорисовать новые данные.
Аналогичные действия выполняем для сделок.
image4611.png
Запустим для проверки.
image944.png

Краткий вывод


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


< 1 2 3 
Replikant

Avatar
Date: 1/22/2019
Reply


Я пришел к варианту: объявить глобальное хранилище свечей и по событию поступление новой свечи туда закидывать свечи, а дальше как хочешь к ним обращаешься. Разработчики что-то говорили про то, что убрали обращение к свечам из-за больших затрат памяти, в общем я не стал дальше разбираться и так сделал.
Thanks:

allint29

Avatar
Date: 1/22/2019
Reply


Я видел прошлую статью о контейнерах свечей, но с ними результат такой же. Не пойму в чем проблема.
Thanks:

Replikant

Avatar
Date: 1/22/2019
Reply


allint29
Я видел прошлую статью о контейнерах свечей, но с ними результат такой же. Не пойму в чем проблема.


Ну я свой класс запилил - класс где одно из полей - массив объектов типа свеча и метод доступа где аргументом задаешь удаление от текущей свечи, а метод возвращает нужную свечу, там уже у неё вытаскиваешь OHLC и т.д.
Thanks:

Replikant

Avatar
Date: 1/22/2019
Reply


Соответственно как появляется новая свеча - туда её складываю. А когда нужно посмотреть нужную свечу на определенном удалении, например, 3 свечи назад - через описанный метод.
Thanks:

bullishbear

Avatar
Date: 10/23/2019
Reply


Replikant
Соответственно как появляется новая свеча - туда её складываю. А когда нужно посмотреть нужную свечу на определенном удалении, например, 3 свечи назад - через описанный метод.


Ага, ну или можно использовать решение на базе queue. Мне например нужно до 20 свечей последних, просто сделал фиксированный стек.
Thanks: lifat

bango666

Avatar
Date: 10/29/2019
Reply


Приветствую всех. Помогите исправить ошибку [img][/img]
вот описание Ошибка CS1503 Аргумент 2: не удается преобразовать из "ShellNew.MainWindow" в "System.Windows.Window". ShellNew C:\Form3Q\ShellNew\ShellNew\MainWindow.xaml.cs 44
ошибка.png 3 KB (282)
Thanks:

bango666

Avatar
Date: 10/31/2019
Reply


Добрый день. Я все с тем же вопросом
Quote:
Приветствую всех. Помогите исправить ошибку ${description}
вот описание Ошибка CS1503 Аргумент 2: не удается преобразовать из "ShellNew.MainWindow" в "System.Windows.Window". ShellNew C:\Form3Q\ShellNew\ShellNew\MainWindow.xaml.cs 44

нашел эту строку в примере SampleMultiConnection написана точно также написана почему показывает ошибку? Умные люди помогите разобраться новичку. И в студии подсказки не помогают.
Thanks:

Михаил

Avatar
Date: 11/7/2020
Reply


В последней версии S#API 5.0.0.17 пример SampleOptionQuoting вообще не запускается,
ошибки компиляции при сборке. Поправьте его пожалуйста.

Severity Code Description Project File Line Suppression State
Error CS0115 'MainWindow.OnClosing(CancelEventArgs)': no suitable method found to override SampleOptionQuoting (net48), SampleOptionQuoting (netcoreapp3.1) c:\stocksharp\StockSharp-5.0.0.17\Samples\Misc\SampleOptionQuoting\MainWindow.xaml.cs 364 Active
Thanks:

Михаил

Avatar
Date: 11/7/2020
Reply


В последней версии S#API 5.0.0.17 пример SampleOptionQuoting вообще не запускается,
ошибки компиляции при сборке. Поправьте его пожалуйста.

Severity Code Description Project File Line Suppression State
Error CS0115 'MainWindow.OnClosing(CancelEventArgs)': no suitable method found to override SampleOptionQuoting (net48), SampleOptionQuoting (netcoreapp3.1) c:\stocksharp\StockSharp-5.0.0.17\Samples\Misc\SampleOptionQuoting\MainWindow.xaml.cs 364 Active
Thanks:

Михаил

Avatar
Date: 11/7/2020
Reply


В последней версии S#API 5.0.0.17 пример SampleOptionQuoting вообще не запускается,
ошибки компиляции при сборке. Поправьте его пожалуйста.

Severity Code Description Project File Line Suppression State
Error CS0115 'MainWindow.OnClosing(CancelEventArgs)': no suitable method found to override SampleOptionQuoting (net48), SampleOptionQuoting (netcoreapp3.1) c:\stocksharp\StockSharp-5.0.0.17\Samples\Misc\SampleOptionQuoting\MainWindow.xaml.cs 364 Active
Thanks:
< 1 2 3 

Attach files by dragging & dropping, , or pasting from the clipboard.

loading
clippy