Блог им. WinterMute

Торговая система своими руками. Часть 9. Отображение результатов. Пример стратегии.

    • 18 октября 2017, 14:27
    • |
    • k100
  • Еще

     Привет всем! В предыдущем посте рассматривались два объекта, которые формируют закрытые позиции и считают статистику торговли (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г. И вот, что наторговал бот-сливала:
Торговая система своими руками. Часть 9. Отображение результатов. Пример стратегии.

     Даже немного в плюс!))

     В следующей статье я расскажу про логирование, что такое IoC контейнер, и как, при помощи одной лишь переменной, менять режимы работы – тестовый или торговый.

★14
7 комментариев
Спасибо! Продолжай)
avatar
А есть возможность запросить какое-то количество исторических тиковых данных и послать их в стратегию в качестве инициализации?
avatar
tranquility, обычно коннекторы поддерживают механизм получения ретроспективных данных, в 4 части данной серии топиков, я описывал способ получения таких данных через асинхронные методы.
avatar
круто блин
avatar
Спасибо за статьи!

Вы сказали, что используете SmartCom. А к чему он подсоединяется (торговый терминал, напрямую к брокеру, бирже)?
 Накопал в инете, так, работая со SmartCom, вы  привязаны к айтиинвест?
Евгений Гуревич, Вы правы, привязан. Но! Т.к. это собственный протокол брокера, то это даёт определённые скоростные и инфраструктурные преимущества. Также, я старался разрабатывать систему так, чтобы, по возможности, не сильно зависеть от брокера и его API — т.е. если потребуется перейти, например на транзак, то это потребует минимальных переделок — код стратегий, работа с БД, локальное протоколирование заявок/сделок — не поменяются.
Хотя оговорюсь, что универсальность тут не к чему — универсальные и гибкие решения дороги в обслуживании и медленные. Всё равно будет какая-то притирка к API брокера, я лишь хотел выделить её в отдельный модуль, сделать просто ещё одним аспектом системы.

avatar

теги блога k100

....все тэги



UPDONW