Блог им. WinterMute
Привет всем! В предыдущем посте рассматривались два объекта, которые формируют закрытые позиции и считают статистику торговли (IClosePositionManager, IResultManager). Сегодняшняя статья будет посвящена визуализации этих данных и общей архитектуре торговой системы.
В своё время я рассказывал про паттерн проектирования MVC, что логика должна быть отделена от визуализации, и ещё, что у каждой формы должен быть свой презентер. Также хотел отметить, что проект лучше разбивать на несколько логических модулей (библиотек классов в c#). Свой проект я разделил на: definitions – содержит базовые, ни от кого не зависящие классы, интерфейсы и описания, local – реализация интерфейсов для локального тестера, smartcom – реализация интерфейсов для коннектора, в данном случае смарткома, strategies – вынес в отдельный модуль все стратегии, UI – внешний интерфейс системы (формы и их презентеры) и т.д. В каждом таком модуле я обычно создаю ещё несколько папок – в модулеUI, например, есть папка interfaces, presenters и views.
Сначала опишу, как отображаются результаты, а потом представлю общую картинку проекта. Итак, понадобится новый презентер, в котором считается статистика (EquityPresenter), и форма к нему (EquityView). Вот код презентера:
public class EquityPresenter { private IEquityView equityView; private IResultManager resultManager; private IClosePositionManager closePositionManager; private IDataService dataService; public EquityPresenter(IEquityView equityView, IResultManager resultManager, IClosePositionManager closePositionManager, IDataService dataService) { this.equityView = equityView; this.resultManager = resultManager; this.closePositionManager = closePositionManager; this.dataService = dataService; } public void CalcAndSetEquity(string symbol, int strategyID) { var orders = dataService .GetOrders(symbol, strategyID); var closePositions = closePositionManager .ClosePositions(orders); var statistics = resultManager .Calc(closePositions); equityView.SetData(statistics, closePositions); } public IEquityView View => equityView; }
В конструкторе инициализируются необходимые в дальнейшем объекты. В методе CalcAndSetEquity, по стратегии, извлекаются из БД заявки и сделки по ним, далее, рассчитываются закрытые позиции и статистика, и, всё это передаётся в форму для отображения. Сама форма, в данном случае, это таблица со списком закрытых позиций, таблица со статистикой и собственно сам график equity. Метод формы SetData, по переданным ему данным заполняет таблицы и строит график.
Теперь, настало время представить проект в целом – сейчас я последовательно опишу полный цикл работы системы, тут будут задействованы все элементы, которые описывались в предидущих постах. Важно отметить, что система может работать как в режиме тестера, так и в режиме реальных торгов, при этом, глобально ничего не меняется – общая картинка остаётся той же, лишь подставляются разные реализации базовых интерфейсов, связанных с подключением, работой с портфелем и получением маркет-даты. После запуска программы, всё управление передаётся MainPresenter’у. Собственно внутри него всё и происходит:
class MainPresenter { public MainPresenter(IMainView mainView, IStrategyFactory strategyFactory, IConnectGate connectGate, IOrderManadger orderManadger, IDataService dataService, ITickGenerator tickGenerator) { connectGate.Connected += () => { // запустили стратегию и ждём маркет-даты strategyFactory .CreateStrategy(1) .Start(); // подписка на событие окончания прогона tickGenerator.onEnd += () => { var orders = orderManadger .getOrders("GAZP", 1); dataService.SaveOrders(orders); // инициализация EquityPresenter’а var equityPresenter = ... equityPresenter.CalcAndSetEquity("GAZP", 1); equityPresenter.View.ShowForm(); // форма с equity } tickGenerator.Start("GAZP"); // начало прогона } connectGate.Connect(); } }
Итак, после запуск программы, и инициализации всех необходимых объектов, происходит подключение к серверу брокера (или имитация подключения в случае с тестером) — connectGate.Connect(). После успешного подключения, при помощи фабрики, инициализируется и запускается стратегия – strategyFactory.CreateStrategy(1).Start(). В конструкторе стратегии происходит настройка параметров и подписка на получение маркет-даты. В случае с реальной торговлей, маркет-дата начинает поступать от брокера. В случае с локальным тестером, маркет-дата вытягивается из БД и запускается процедура генерации тиковых (либо иных) сигналов — tickGenerator.Start(). Всё, робот начал торговать! Теперь осталось дождаться события окончания прогона. Инициатором такого события может быть: нажатие кнопки “стоп” на форме, какая-то временная отсечка, а в случае с локальным тестером – это просто исчерпывание локальной маркет-даты. После окончания прогона, по стратегии, получаем совершённые заявки и сделки по ним — orderManadger.getOrders(), и, сохраняем всё это в базу — dataService.SaveOrders(). Далее, инициализируется EquityPresenter, и, происходит подсчёт статистики – equityPresenter.CalcAndSetEquity(), теперь, осталось только вывести результаты на экран – equityPresenter.View.ShowForm().
И, напоследок, небольшой пример стратегии. Описание самих стратегий выходит за рамки данной серии статей, поэтому приведу пример реализации самой простой, сливной стратегии, лишь для того, чтобы продемонстрировать, что весь механизм работает и какой строится итоговый график.
Суть стратегии – пробой предыдущего часового бара. На тиковых данных считается максимум и минимум предыдущего часа, если текущая цена пробила максимум – покупка, если пробила минимум — продажа. Выход из позиции: либо по стопу в 1% от цены, либо по тейку в 3% от цены. Комисы 0.035% на круг, проскальзывание не учитывается. Гоняем один лот. Ниже представлен код, реализующий эту стратегию:
public class SimpleStrategy : IStrategy { private IMarketDataGate marketDataGate; private IOrderManager orderManager; private int strategyID = 1; private string symbol = "GAZP"; private double prewHight = 0; private double prewLow = 999; private double curHight = 0; private double curLow = 999; private int amount = 10; private int positionDirection = 0; private double take; private double stop; private DateTime updateDate; public SimpleStrategy ( IMarketDataGate marketDataGate, IOrderManager orderManager) { this.marketDataGate = marketDataGate; this.orderManager = orderManager; } public void Start() { marketDataGate.AddTick += AddTickHandler; marketDataGate.ListenTicks(symbol); } private void AddTickHandler(object o, AddTickEventArgs e) { if (e.DateTime > updateDate) { prewHight = curHight; prewLow = curLow; curHight = e.Price; curLow = e.Price; updateDate = e.DateTime.AddMinutes(60); } if (e.Price > curHight) curHight = e.Price; else if (e.Price < curLow) curLow = e.Price; if (positionDirection == 0 && e.Price > prewHight) { orderManager.PlaceOrder(symbol, strategyID, OrderAction.Buy, OrderType.Market, amount); positionDirection = 1; take = e.Price + e.Price * 0.03; stop = e.Price - e.Price * 0.01; } else if (positionDirection == 0 && e.Price < prewLow) { orderManager.PlaceOrder(symbol, strategyID, OrderAction.Sell, OrderType.Market, amount); positionDirection = -1; take = e.Price - e.Price * 0.03; stop = e.Price + e.Price * 0.01; } else if (positionDirection == 1 && (e.Price > take || e.Price < stop)) { orderManager.PlaceOrder(symbol, strategyID, OrderAction.Sell, OrderType.Market, amount); positionDirection = 0; } else if (positionDirection == -1 && (e.Price < take || e.Price > stop)) { orderManager.PlaceOrder(symbol, strategyID, OrderAction.Buy, OrderType.Market, amount); positionDirection = 0; } } }
В методе Start происходит подписка на тиковые данные, далее, стратегия ожидает их прихода. На каждый тик выполняется сам алгоритм бота: если прошёл очередной час – устанавливаются новые границы канала (high и low предыдущего часового бара). Если позиция не открыта (positionDirection == 0) и произошёл пробой (либо вверх, либо вниз), то соответствующая позиция открывается. Если позиция уже открыта и цена достигла значения take или stop – позиция закрывается. Код простейший, и много чего не учитывает, например, перенос через ночь, лимит времени в позиции, и, многие другие аспекты торговли. Но думаю, общая картинка понятна. Тут используются описанные ранее IMarketDataGate, для получения маркет-даты и, IOrderManager, для выставления и протоколирования заявок. Тестирование проводилось по акциям Газпрома с мая по июнь 2017г. И вот, что наторговал бот-сливала:
Даже немного в плюс!))
В следующей статье я расскажу про логирование, что такое IoC контейнер, и как, при помощи одной лишь переменной, менять режимы работы – тестовый или торговый.
Вы сказали, что используете SmartCom. А к чему он подсоединяется (торговый терминал, напрямую к брокеру, бирже)?
Хотя оговорюсь, что универсальность тут не к чему — универсальные и гибкие решения дороги в обслуживании и медленные. Всё равно будет какая-то притирка к API брокера, я лишь хотел выделить её в отдельный модуль, сделать просто ещё одним аспектом системы.