Блог им. 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 брокера, я лишь хотел выделить её в отдельный модуль, сделать просто ещё одним аспектом системы.