пятница, 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 импорта транзакций"). В итоге получился следующий функционал:

/// <summary>
/// Основной класс, предоставляющий шлюз взаимодействия с Quik.
/// </summary>
public class Trader
{
/// <summary>
/// Инициализация класса Trader.
/// </summary>
/// <param name="path">Путь к директории, где установлен Quik.</param>
/// <param name="ddeServer">Название DDE сервера.</param>
public Trader(string path, string ddeServer);

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

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

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

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

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

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

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

/// <summary>
/// Список всех загруженных инструментов.
/// Вызывать только после того, как пришло событие <see cref="SecuritiesLoaded" />.
/// </summary>
public IEnumerable<Security> Securities;

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

/// <summary>
/// Получить все зявки, которые были зарегистрированный программой через метод <see cref="RegisterOrder" />.
/// </summary>
public IEnumerable<Order> Orders;

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

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


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

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

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

/// <summary>
/// Получить заявку по сделке.
/// </summary>
/// <param name="trade">Сделка, по которой нужно искать заявку.</param>
/// <returns>Найденная заявка.</returns>
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.

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

  1. Добрый день, Михаил.
    Настроил все так как Вы написали.
    Quik начинает передавать данные, и сразу возникает следующая ошибка.
    Можете ли подсказать причину ошибки?

    Максим



    System.ArgumentOutOfRangeException was unhandled by user code
    Message="Index was out of range. Must be non-negative and less than the size of the collection.\r\nParameter name: index"
    Source="mscorlib"
    ParamName="index"
    StackTrace:
    at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
    at System.ThrowHelper.ThrowArgumentOutOfRangeException()
    at System.Collections.Generic.List`1.get_Item(Int32 index)
    at Ecng.Quik.Wrapper.Trader..(String )
    at Ecng.Collections.CollectionHelper.SafeAdd[K,V](IDictionary`2 dictionary, K key, Func`2 handler) in E:\!Soft\.NET\Ecng\Collections\CollectionHelper.cs:line 228
    at Ecng.Quik.Wrapper.Trader.(String , IList`1 )
    at .(Object )
    InnerException:

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

    ОтветитьУдалить
  3. Максим, если включать экспорт по очереди (сначала инструмменты, сделки, и т.д.), то на каком этапе происходит ошиба?

    Исходники, к сожалению, недоступны по ряду причин:

    1) Хочу сделать общую библиотеку, которая поможет разрабатывать роботов под Квик. Если выложить исходники, то многие будут исправлять ошибки в тихую. А хочется что-то централизированное, заводы - рабочим, земли кестьянам, QuikWrapper - трейдерам.

    2) Я планирую развивать систему (подчеркиваю, именно систему, с базой историчности и тестированием). Так что, возможно, в нее войдут уже ряд коммерческим блоков, которые уж никак я не имею права выкладывать.

    В скором будущем (несколько дней) я выложу новую версию. Будут доступны механизму по урпавлению DDE и свечки.

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

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

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

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

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

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

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

    ОтветитьУдалить
  5. 1. Такая ошибка может произойти если не все колонки добавлены. Все 15 колонок добавлены в таблицу?

    2. Ок, по пунктам:

    а) Вот поэтому я стараюсь не делать логики в библиотеке. Пусть логику добавляют те, кто будут пользовать этой библиотекой.

    б) Я акцентируюсь только на разработчиков .NET. В принципе, можно поддержать и Java (про другие менее популярные языки я не буду высказываться), но я с ней плохо знаком.

    в) Хорошая мысль, надо подумать над ней.

    г) Я пока не думал на предмет монетизации. Скорее всего, все останется как есть сейчас - бесплатное. Коммерческое можно сделать и по другому...

    ж) Максим, пункт в) меня навел на мысль. Ушел думать.

    ОтветитьУдалить
  6. 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:

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

    trader.RegisterOrder(order);

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

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

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

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

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

    ОтветитьУдалить
  12. Этот комментарий был удален автором.

    ОтветитьУдалить
  13. Максим, вот то, о чем я писал в самом начале. Каждый начинает писать свое, в итоге все тихо сидят со своим функционалом. Давайте по порядку - чем для вашей задачи не подходит QuikWrapper?

    ОтветитьУдалить
  14. 1)Насколько я понимаю цель опенсорсных проектов, это максимизация общей пользы.

    Если открыть код, то он поможет и тем кто его будет использовать полностью, и тем, кто будет его использовать частично, и тем, кто его не будет использовать непосредственно (например, для обучения). Польза всем.

    Если же код оставить закрытым, то пользу получат только те, кто будут непосредственно использовать библиотеку. Все остальные окажутся «за бортом». Польза меньше, чем в первом случае.

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


    2) Что касается меня.
    На данный момент я не могу четко сформулировать что именно мне надо.
    Буквально неделю назад я разрабатывал систему на языке Mathematica.
    Но к сожалению для разработки программы для онлайн анализа данных этот язык не подошел.
    Сейчас разбираюсь в C#.
    Мне нужно перенести мой функционал на C#.
    С# изучаю с нуля.
    В этом бы Ваш код очень бы помог и ускорил мою работу.
    С экспортом заявок все понятно.
    Не могу победить DDE.

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

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

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

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

    ОтветитьУдалить
  16. 2First
    Большое спасибо за ссылку.
    Буду разбираться.

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

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