пятница, 18 сентября 2009 г.

QuikWrapper

Обновление! Данный проект был полностью переделан. Описание нового проекта с документацией и дистрибутивом находиться по адресу http://stocksharp.com.

На днях я, наконец-то, разобрался с системой экспорта данных из Quik по DDE (результаты качать отсюда). Парадокс построения МТС на базе Quik таков, что довольно легко отправить в него данные (заявки), но сложно получить что-то обратно (информацию по инструментам, тикам и т.д.). Стандартно механизм отдачи данных внешней программе построен на специальных адаптерах (для AmiBroker и Wealth-Lab идут стандартно в поставке), но если нужно действительно автономное решение (программирование роботов на C#, Java или Delphi), то начинаются танцы с бубном. Для сведения, протокол DDE - это старая технология. Настолько старая, что мало того, на данный момент в новых средах программирования (.NET или Java) не существует стандартных компонентов для работы с DDE, так еще и описаний в инете практически не найти.

В принципе, экспорт данных из Quik можно настроить через ODBC. Но у него есть ряд своих недостатков. Во-первых, на компьютере должна быть установлена база данных (мой выбор, MS Sql Server 2008 Express - быстр, стабилен, много возможностей, и, конечно же, бесплатен). Во-вторых, это медленная передача данных. В отличие от DDE, где данные передаются напрямую от Quik к сторонней программе, использование промежуточной базы данных сильно затормаживает и скорость самой МТС.

Есть еще последний вариант, когда можно написать МТС внутри Quik-а на языке QPile, но я его не рассматривал, так как это очень медленное решение, вдобавок и ограниченное возможностями самого языка (на дворе 21-ый век, и нужно использовать компьютеризированные возможности по максимуму).

Собственно, для того, чтобы каждый раз новичкам не проходить тернистый путь написания МТС на языке C# (или любом другом под платформу .NET), я и написал библиотеку QuikWraper. За основу был взят пример работы с Quik API (скачать можно отсюда http://www.quik.ru/user/download/, называется "API импорта транзакций"). В итоге получился следующий функционал:
/// 
/// Основной класс, предоставляющий шлюз взаимодействия с Quik.
/// 
public class Trader
{
/// 
/// Инициализация класса Trader.
/// 
/// Путь к директории, где установлен Quik./// Название DDE сервера.public Trader(string path, string ddeServer);

/// 
/// Событие изменения состояния подключения. Срабатывает при первом подключении программы к Quik-у,
/// сигнализирую о том, что соединение установлено.
/// 
public event Action ConnectionChanged;

/// 
/// Ошибка при обработке DDE данных, посланых Quik-ом.
/// 
public event Action DdeError;

/// 
/// Событие появления собственных новых сделок.
/// 
public event Action> NewMyTrades;

/// 
/// Событие появления всех новых сделок.
/// 
public event Action> NewTrades;

/// 
/// Событие появления новых заявок.
/// 
public event Action> NewOrders;

/// 
/// Событие изменения состояния заявки (снята, удовлетворена).
/// 
public event Action> OrdersChanged;

/// 
/// Событие загрузки данных по инструментам.
/// 
public event Action SecuritiesLoaded;

/// 
/// Список всех загруженных инструментов.
/// Вызывать только после того, как пришло событие .
/// 
public IEnumerable Securities;

/// 
/// Получить биржевое время.
/// 
public DateTime StockTime;

/// 
/// Получить все зявки, которые были зарегистрированный программой через метод .
/// 
public IEnumerable Orders;

/// 
/// Получить мои сделки.
/// 
/// Инструмент, по которому нужно найти сделки./// Дата, с которой нужно искать сделки./// Дата, до которой нужно искать сделки./// Найденные сделки.
public IEnumerable GetMyTrades(Security security, DateTime from, DateTime to);

/// 
/// Получить все сделки.
/// 
/// Инструмент, по которому нужно найти сделки./// Дата, с которой нужно искать сделки./// Дата, до которой нужно искать сделки./// Найденные сделки.
public IEnumerable GetTrades(Security security, DateTime from, DateTime to);


/// 
/// Получить стакан котировок.
/// 
/// Инструмент, по которому нужно получить котировки./// Найденные котировки. Если для инструмента нет котировок, то возвращается пустой список.
public IEnumerable GetStock(Security security);

/// 
/// Зарегистрировать заявку на бирже.
/// 
/// Заявка, содержащая информацию для регистрации.public void RegisterOrder(Order order);

/// 
/// Отменить заявку на бирже.
/// 
/// Заявка, которую нужно отменять.public void CancelOrder(Order order)

/// 
/// Получить заявку по сделке.
/// 
/// Сделка, по которой нужно искать заявку./// Найденная заявка.
public Order GetOrder(Trade trade);
}

В качестве теста я создал консольное приложение. Программа находит бумагу Лукойл, запоминает первоначальное значение середины спреда равное (bid + ask) / 2. Далее, как только значение спреда отклониться на 0.1 %, то выставляется заявка на покупку объемом 1 и ценой текущего спреда. Далее, если произойдет сделка по выставленной заявке, то программа выведет информацию по этой сделке. Вот текст программы:
// для теста выбираем бумагу Лукойл
var secCode = "LKOH";
Security lkoh = null;

// номер счета
var account = "XXX";

var waitHandle = new ManualResetEvent(false);

// создаем соединение с Quik-ом
using (var trader = new Trader(@"D:\QUIK5", "wrapper"))
{
  // подписываемся на событие появление инструментов
  trader.SecuritiesLoaded += () =>
  {
      // находим Лукойл и присваиваем ее переменной lkoh
      lkoh = trader.Securities.First(sec => sec.Code == secCode);

      Console.WriteLine("Инструмент Лукойл появился");
      waitHandle.Set();
  };

  // подписываемся на событие появления моих новых сделок
  trader.NewMyTrades += trades =>
  {
      foreach (var trade in trades)
          Console.WriteLine("Сделка {0} по цене {1} по бумаге {2} по объему {3} в {4}", trade.Id, trade.Price, trade.Security.Code, trade.Volume, trade.Time);
  };

  Console.WriteLine("Дожидаемся появления в программе инструмента Лукойл");
  waitHandle.WaitOne();

  if (lkoh != null)
  {
      // 0.1% от изменения цены
      var delta = 0.001;

      // запоминаем первоначальное значение спреда
      var firstMid = lkoh.BidAsk.Mid;
      Console.WriteLine("Первоначальное значение спреда {0}", firstMid);

      while (true)
      {
          // если спред вышел за пределы нашего диапазона
          if    (
                  ((firstMid + firstMid * delta) <= lkoh.BidAsk.Mid) ||
                  ((firstMid - firstMid * delta) >= lkoh.BidAsk.Mid)
              )
          {
              var order = new Order
              {
                  Account = account,
                  Price = lkoh.BidAsk.Mid,
                  Security = lkoh,
                  Volume = 1,
                  Direction = OrderDirections.Buy,
              };
              trader.RegisterOrder(order);
              Console.WriteLine("Заявка {0} зарегистрирована", order.Id);
              break;
          }
          else
              Console.WriteLine("Текущее значение спреда {0}", lkoh.BidAsk.Mid);
          // ждем 1 секунду
          Thread.Sleep(1000);
      }
  }
  else
      Console.WriteLine("Инструмент Лукойл не появился");

  Console.WriteLine("Заканчиваем работу. Нажмите кнопку чтобы продолжить");
  Console.Read();
}


Последнее, о чем необходимо еще упомянуть - это настройка самого Quik. Процесс несколько нудный, но, к счастью, делать нужно один раз. Самое первое, включить поддержку Внешних Транзакций. Пункт меню Торговля -> Внешние Транзакции и сделать так, как показано на рисунке:

И теперь DDE:
Инструменты


Все сделки


Заявки


Мои сделки


Важно! Колонки в таблицах должны идти так, как показаны на рисунках. Можно добавлять свои собственные колонки в конец, но никак не перемешивать с нужными. Запускать вывод через DDE тоже необходимо делать в строго определенном порядке. Сначала инструменты, затем все сделки, затем заявки, затем мои сделки.

Исходники примера, а так же сама библиотека с документацией располагается здесь:
http://www.box.net/shared/o7et6ac56x.

12 комментариев:

  1. Михаил, можете ли выложить исходники Вашей библиотеки QuikWraper?

    ОтветитьУдалить
  2. 1) Делаю следующее:
    а) Запускаю Вашу программу.
    б) Запускаю экспорт Инструментов.
    в) Проходит пару секунд и появляется вышеуказанная ошибка.

    2) Насчет Ваших намерений, глубокая Вам признательность и уважение.
    Поделюсь своими мыслями на сей счет:

    а) Универсальную библиотеку сделать не получится, так как у всех свои требования к библиотеке. Многим будет достаточно Вашего функционала, но тем, кому не достаточно, придется проделывать всю Вашу работу самостоятельно.

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

    в) Для того, что бы тестировать библиотеку на глюки, надо попросить всех сообщать о них. Думаю сообщество достаточно сознательное, что бы не " исправлять ошибки в тихую".

    г) Для монетизации проекта коммерческие блоки можно выделить их в отдельные библиотеки.

    ж) Это все, естественно, мой взгляд на вещи. Вполне разделяю Ваше право поступать так как хотите.

    ОтветитьУдалить
  3. 1) Действительно, Вы были правы, пропусти один столбец (хотя проверял до этого).
    Вышеуказанная ошибка исчезла.

    2) После запуска программы и запуска экспорта по DDE (в том порядке, который Вы указали), программа работает около минуту, в консоли появляются строчки "Текущее значение спреда 1758,715", но потом возникает следующая ошибка (указана ниже). Я опять делаю что то не то или это действительно ошибка?

    3) Для плодотворной разработки продукта, стоит внедрить какую либо систему багтрекинга. Тогда работа будет коллективная и удобная. Существуют открытые системы багтрекинга, например:
    http://code.google.com/intl/ru/projecthosting/
    http://en.wikipedia.org/wiki/Comparison_of_open_source_software_hosting_facilities

    Ecng.Quik.Wrapper.QuikApiException was unhandled
    Message="Код ошибки WrongSyntax Сообщение "
    Source="Ecng.Quik.Wrapper"
    StackTrace:
    at .(Int64 , StringBuilder )
    at .(String , OrderStatus& , Int32& , Double& , String& )
    at Ecng.Quik.Wrapper.Trader.RegisterOrder(Order order)
    at SampleConsole.Program.Main() in D:\Project\C#\Redist\Redist\SampleConsole\Program.cs:line 70
    at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
    at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
    at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
    at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    at System.Threading.ThreadHelper.ThreadStart()
    InnerException:

    ОтветитьУдалить
  4. Ошибка подсвечивает вот эту строчку:

    trader.RegisterOrder(order);

    ОтветитьУдалить
  5. У меня такое было, когда я неправильный счет указывал (вместо счета указывал логин к квику). Если с этим все нормально, то отпишитесь, по какому инструменту создаете заявку.

    ОтветитьУдалить
  6. Да, действительно, указывал неправильный счет.

    ОтветитьУдалить
  7. Михаил, что Вы решили насчет совместной разработки проекта?

    Будете самостоятельно продолжать его разрабатывать или откроете его для совместной работы и отладки?

    ОтветитьУдалить
  8. Шаг за шагом. Выложу новую версию, подумаю над общей разработкой (по моему опыту, это не просто сделать). Времени свободного не так много, поэтому приходится чем-то жертвовать (или ставить в очередь).

    ОтветитьУдалить
  9. Михаил, добрый день.

    1) Помимо данного сайта, есть ли другой способ общения с Вами? Например, почта, ICQ, Skype.

    2) Как Ваша библиотека обрабатывает событие потери связи Квик-Сервер, Библиотека-Квик?

    3) При наступлении события Trader.NewTrades передается переменная /IEnumerable/Trade//. При старте программы данное событие передает очень много сделок. Правильно ли я пониманию, что передаются все сделки, которые есть на данные момент в «таблице всех сделок» Квика ?

    ОтветитьУдалить
  10. Подскажите, пожалуйста, а возможно ли запустить паралельно нескольких роботов от одного квика на одном компьютере??? или же чтобы одна и таже стратегия просчитывалась для разных таймфреймов и по разным бумагам??

    ОтветитьУдалить
  11. Используйте класс Strategy. А вообще есть отдельный форум для таких вопросов - http://groups.google.ru/group/stocksharp

    ОтветитьУдалить