Блог им. WinterMute

Торговая система своими руками. Часть 4. Локальная маркет-дата. Семафоры.

    • 11 сентября 2017, 14:23
    • |
    • k100
  • Еще

     Привет всем! В предыдущий раз я описал, как стратегии выставляют заявки. Сегодня будет ещё более интересная тема: получение маркет-даты. Для упрощения, под маркет-датой, буду иметь в виду тиковые данные (время, цена, объём).

     Я уже рассказывал про классы стратегий,  про то, что они используют интерфейс, который отвечает за получение маркет-даты – IMarketDataGate. Внутри себя, стратегии подписываются на событие AddTick из IMarketDataGate – т.е. на каждый тик стратегия проводит свой анализ данных, расчеты, и, при определённых условиях, выставляет заявки. Стратегии не важно, как генерируются тики – она просто реагирует на это событие. IMarketDataGate, имеет два варианта реализации. Первый – это обёрткой над COM библиотекой брокера (в моём случае – смартком). Тут всё просто – каждый день, кроме праздников и выходных, с 10 часов, магическим образом, начинают литься тики – их мне посылает система брокера. А вот для организации локальных бэктестов, нужен какой-то иной источник данных – некая имитация брокера по части генерации тиков. И тут-то и появляется наш герой – ITickGenerator.

interface ITickGenerator
{
   event EventHandler<StockTickEventArgs> OnTick;
   event Action OnEnd;
   void Start(string symbol);
}

     Два события и метод для запуска генератора. EventHandler<StockTickEventArgs> – это обобщённый делегат типа StockTickEventArgs (класс описывающий тик). В результате, в случае с локальным бэктестером, IMarketDataGate получает данные именно от TickGenerator’а, и транслирует их подписавшимся стратегиям.

     Но это не всё, есть ещё одна загвоздка. В предыдущем посте, я описывал IOrderManager, при помощи которого стратегии выставляют заявки. Вопрос в том, как узнать цену исполнения, когда стратегией выставляется заявка? При работе с API брокера такой проблемы нет, но в случае с локальным тестером, класс, который отвечает за приём заявок, тоже должен знать актуальную цену. Для этого, он тоже должен использовать TickGenerator и каждый тик обновлять у себя эту цену (в моём случае, это класс LocalPortfolioGate имплементирующий IPortfolioGate).

     Реализации интерфейса ITickGenerator могут быть разнообразными. Например, данные могут приходить от удалённого сервиса или генерироваться случайным образом,  на лету. Или, например, из БД – получили выборку, и в цикле по ней вызвали событие OnTick, оповестив этим всех подписчиков.

     Но, как понять, что все тики прошли и прогон окончен? Изначально, в конце прогона, я передавал тик со специфическими данными – например с ценой=-999, а в коде стратегий проверял – если пришла такая необычная цена, то прогон окончен. Это не совсем верный путь. Получается, стратегия “знает” о специфике реализации TickGenerator’а, и тем более, это не имеет смысла вне рамок бэктестов. Впоследствии я нашёл другое решение: в конце прогона просто приходит событие OnEnd, и на этом тестирование заканчивается, и проводятся подсчёты. Это один из примеров того, что структура программы может меняться, и это не страшно! Программист должен быть готов к тому, чтобы постоянно дорабатывать код, переписывая его и делая более совершенным. Любая часть может быть взята и переписана. Если что-то находится под грифом “не трогай! Это непонятно, как работает и ладно!”, то рано или поздно, именно эта часть и даст сбой. Гибкая разработка и Agile, в разумных границах применения, позволяет, в конце концов, раскрывать всю скрытую мощь вашего кода.

     Теперь, с учётом TickGenerator’а, можно ещё раз представить общий каркас программы.

class MainPresenter
{
   private IMainView mainView;
   private IStrategyFactory strategyFactory;
   private IConnectGate connectGate;
   private ITickGenerator tickGenerator;

   public MainPresenter(IMainView mainView,
     IStrategyFactory strategyFactory,
     IConnectGate connectGate,
     ITickGenerator tickGenerator)
   {
      this.mainView = mainView;
      this.strategyFactory = strategyFactory;
      this.connectGate = connectGate;
      this.tickGenerator = tickGenerator;
      
      this.connectGate.Connected += () =>
      {
        this.strategyFactory
        .CreateStrategy(1)
        .Start(); // запустили стратегию и ждём тиков
        
        tickGenerator.onEnd += () => {} // подписались на окончание прогона - тут будет расчёт equity
        tickGenerator.Start("SBER"); // начало прогона
      }

      this.connectGate.Connect();
   }

   public IMainView View => mainView;
}

     Мы не знаем о том, откуда берутся тики – подключившись, мы создаём экземпляр стратегии, запускаем генератор и ловим событие окончания прогона, чтобы подсчитать результаты.

     Ещё, очень коротко, хотел рассказать про многопоточность. Её обязательно надо использовать, если одновременно торгует несколько стратегий, когда происходит конкурентный доступ к разделяемым ресурсам программы, или просто необходимо распараллелить задачи. Для организации подобных процессов C# предлагает ряд инструментов, в первую очередь, это async/await методы, класс Task, обобщенные TaskCompletionSource<T>, AutoResetEvent’s, семафоры и т.д. Приведу пример: надо получить цену открытия двух предыдущих минутных баров, и, дождавшись этого, что-то сделать.

…
SemaphoreSlim semaphoreSlim;
List<int> arrayOfPrices = new List<int>();

private async List<int> GetBarsAsync()
{
  semaphoreSlim = new SemaphoreSlim(0, 1);

  marketDataGate.AddBar += AddBarHandler;
  marketDataGate.GetBars(“SBER”, 
    BarInterval._1Min, DateTime.Now, 2);

  await semaphoreSlim.WaitAsync();
  return arrayOfPrices;
}

private void AddBarHandler(object o, AddBarEventArgs e)
{
  if (e.Row != e.NRows - 1)
    arrayOfPrices.add(e.Open);
  else semaphoreSlim.Release();
}

     Ключевое слово async указывает, что метод должен выполняться в отдельном потоке, а ключевое слово await приостанавливает это выполнение и передаёт управление основному потоку.  Внутри GetBarsAsync, сперва инициализируем семафор – счётчик, в диапазоне от 0 до 1. Далее, подписываемся на событие получение баров, и начинаем их получение. Асинхронно ждём, пока семафор не дойдёт до своего крайнего значение. Асинхронно, это значит, что код родительского потока не блокируется и метод AddBarHandler будет вызван, но, в тоже время, пока семафор не высвободится, выполнение метода semaphoreSlim.WaitAsync() не прекратиться.

★5
7 комментариев
У меня вот два вопроса возникло…
1. а почему бы не сделать внешний генератор данных, подключение ыло бы по сети и там бы уже иммитировали бы работу биржи, с тиками, данными и т.д. заодно можно было бы обрывы и ошибки соединений отладить.?

2. а нужен ли симофор если вы данные только добавляете?
avatar
Denis, 
***** Автор по-любому жестит в приведенном коде. Да в тексте.

avatar
mr lab, ну код я не берусь разбирать, сам подход в общем то выглядит верным.
А про тестовый сервер стоит подумать, возможно это в двое больше работы, зато иммитация куда реальнее, как мне кажется. Но возможно я не вижу всех плюсов и минусов, вот и хотелось бы обсудить )
avatar
Denis, Можно было бы написать отдельное приложение — некий виртуальный сервис, который бы по http протоколу оповещал бы подписчиков — слал бы данные в формате json. Это, так называемая сервис ориентированная архитектура, микро-сервисы, сокеты, WCF и т.п. Тут, я просто не хотел усложнять данный пример.

По поводу семафоров — есть принципиально иной подход ко всему этому — функциональный, основанный, например, на callback функциях. Он, кстати, мне ближе. 

avatar
Очень интересные статьи.
А ваш проект где-нибудь выложен ( на гитхабе или еще где) чтобы можно было скомпилировать? Было бы здорово.
avatar
WildTrader, Спасибо! Мне есть ещё что сказать в последующих постах! По поводу — выложить на гитхаб — а что! Пожалуй и выложу!
avatar

теги блога k100

....все тэги



UPDONW
Новый дизайн