__rtx
__rtx личный блог
11 июля 2025, 07:20

Алготрейдинг. OsEngine покоряет новые высоты(только не будем уточнять откуда мерить).

Алексей продолжает серию юмористических постов о том как они идут в колокацию и HFT. Ок, давайте тогда немного поможем коллегам из OsEngine сократив им этот не лёгкий путь накидав немного советов. И возможно(но это не точно) качество кода вырастет(этот пост удалять не буду как обычно) в общем мотивация для команды OsEngine и Алексея развиваться. Не благодарите за бесплатную помощь, я от всей души.

Беглый взгляд на код на гитхабе сразу цепляет мой «токсичный» глаз и не отпускает его почти всё время пока смотришь их код. Куча потенциальных(и не потенциальных) проблем.

Совет номер раз.
— перечитать умные книги по программированию и архитектуре ПО. В частности полюбить один из постулатов «правильных движений» таких как «DRY» или «донт репит ёрселф» или «не повторяйся» тогда кол-во [эскузэмуя]го.н.кода станет сильно меньше чем у Вас сейчас(и 30 000 строк кода у Вас в тестере превратится в кратно меньшее число уменьшив вероятность потенциальных ошибок, упростив поддержку кода для Ваших программистов(которых Вы выручали с иллюстрациями как этот процесс проходил«smart-lab.ru/company/os_engine/blog/1042133.php»)). «DRY» это примерно так:
Ваш код сейчас:

… DataGridViewColumn newColumn0 = new DataGridViewColumn();
newColumn0.CellTemplate = cellParam0;
newColumn0.HeaderText = «Order is active?»;
_gridDataGrid.Columns.Add(newColumn0);
newColumn0.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn1 = new DataGridViewColumn();
newColumn1.CellTemplate = cellParam0;
newColumn1.HeaderText = «Order direction»;
_gridDataGrid.Columns.Add(newColumn1);
newColumn1.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn2 = new DataGridViewColumn();
newColumn2.CellTemplate = cellParam0;
newColumn2.HeaderText = «First order price»;
_gridDataGrid.Columns.Add(newColumn2);
newColumn2.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn3 = new DataGridViewColumn();
newColumn3.CellTemplate = cellParam0;
newColumn3.HeaderText = «Orders count»;
_gridDataGrid.Columns.Add(newColumn3);
newColumn3.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn4 = new DataGridViewColumn();
newColumn4.CellTemplate = cellParam0;
newColumn4.HeaderText = «Step type»;
_gridDataGrid.Columns.Add(newColumn4);
newColumn4.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn5 = new DataGridViewColumn();
newColumn5.CellTemplate = cellParam0;
newColumn5.HeaderText = «Step»;
_gridDataGrid.Columns.Add(newColumn5);
newColumn5.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn6 = new DataGridViewColumn();
newColumn6.CellTemplate = cellParam0;
newColumn6.HeaderText = «Profit type»;
_gridDataGrid.Columns.Add(newColumn6);
newColumn6.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn9 = new DataGridViewColumn();
newColumn9.CellTemplate = cellParam0;
newColumn9.HeaderText = «Profit»;
_gridDataGrid.Columns.Add(newColumn9);
newColumn9.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn7 = new DataGridViewColumn();
newColumn7.CellTemplate = cellParam0;
newColumn7.HeaderText = «Volume type»;
_gridDataGrid.Columns.Add(newColumn7);
newColumn7.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

DataGridViewColumn newColumn8 = new DataGridViewColumn();
newColumn8.CellTemplate = cellParam0;
newColumn8.HeaderText = «Volume»;
_gridDataGrid.Columns.Add(newColumn8);
newColumn8.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; ...


Ваш код после «DRY»:

...Action<string> addColumn = headerText => {
var col = new DataGridViewColumn {
CellTemplate = cellParam0,
HeaderText = headerText,
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
};
_gridDataGrid.Columns.Add(col);
};

addColumn(«Order is active?»);
addColumn(«Order direction»);
addColumn(«First order price»);
addColumn(«Orders count»);
addColumn(«Step type»);
addColumn(«Step»);
addColumn(«Profit type»);
addColumn(«Profit»);
addColumn(«Volume type»);
addColumn(«Volume»);...


Ну не красота ли? Почему это важно если убрать что сильно сокращает кол-во кода и делает его поддержку сильно проще и приятней? Допустим при очередном рефакторинге или добавлении/удалении какой-то функциональности кто-то из Ваших коллег забыл поменять цифру и появилась ошибка. У Вас по всему коду такое. Вот например ещё(просто пальцем ткни и попадёшь на такие моменты)


… for (int i = 0; i < Lines.Count; i++)
{
DataGridViewRow rowLine = new DataGridViewRow();

rowLine.Cells.Add(new DataGridViewTextBoxCell());
rowLine.Cells[0].Value = i + 1;

DataGridViewComboBoxCell cell1 = new DataGridViewComboBoxCell();
cell1.Items.Add(true.ToString());
cell1.Items.Add(false.ToString());
cell1.Value = Lines[i].IsOn.ToString();
cell1.ReadOnly = false;
rowLine.Cells.Add(cell1);

rowLine.Cells.Add(new DataGridViewTextBoxCell());
rowLine.Cells[2].Value = Math.Round(Lines[i].PriceEnter, 10);
rowLine.Cells[2].ReadOnly = false;

rowLine.Cells.Add(new DataGridViewTextBoxCell());
rowLine.Cells[3].Value = Math.Round(Lines[i].PriceExit, 10);
rowLine.Cells[3].ReadOnly = false;

rowLine.Cells.Add(new DataGridViewTextBoxCell());
rowLine.Cells[4].Value = Math.Round(Lines[i].Volume, 10);
rowLine.Cells[4].ReadOnly = false;

rowLine.Cells.Add(new DataGridViewTextBoxCell());
rowLine.Cells[5].Value = Lines[i].Side;

rowLine.Cells.Add(new DataGridViewCheckBoxCell());
rowLine.Cells[6].Value = Lines[i].checkStateLine;
rowLine.Cells[6].ReadOnly = false;

_gridDataGrid.Rows.Add(rowLine);
}...


Это ботва называется.))) Knight Capital потеряла 400 млн. долларов из-за того что кто-то забыл обновить ПО на одном из серверов. Т.е. чтобы сократить вероятность ошибок люди используют «DRY». А Вы?


Совет номер два.
— не ленитесь использовать лямбда функции. Их использование делает код элегентным и читаемым(это как инвестиция в него т.к. потраченное время сейчас чтобы подумать в будущем позволяет не тратить на это время чтобы понять что тут делается, отловить ошибки и т.д.) + лямбда функции не засоряют документацию т.к. используются ровно там где нужны и не больше. Т.е. на гитхабе будет меьше названий методов а в документации не надо думать и описывать метод которые имеет локальное назначение в несколько «микромоментов» и не несёт никакой пользы тому кто читает про него и видит его описание. Когда кол-во кода(как у Вас) увеличивается в геометрической прогрессии такой код надо по максимуму ограждать от потенциальных проблем. На заметку Вам и Вашим «технологическим спецназовцам»(как Вы сами у себя в постах себя пытаетесь преподнести) --> Если при написании кода руки хотят «замиксовать» число со словом сначала подумайте возможно тут надо замутить лямбда функцию либо обычную если то что делает код делает ещё какой-то кусок в коде(в другом месте). Числа и слова так лучше не делать.

Совет номер три.
— не работайте со строками в местах где не надо с ними работать например в одном из коннекторов(где в моменты движений будет очень горячо) у Вас сравниваются строки:

...List<OrderChange> bidsByPrice = orderChanges.FindAll(p => p.OrderType == «bid»);...


как я писал в прошлом посте всего лишь надо сделать(в с++ например так)

...enum class Direction: uint8_t {
BID = 1,
ASK = 2
}

и List<OrderChange> bidsByPrice = orderChanges.FindAll(p => p.OrderType == BID);


Ок что там у нас по плазе интересно?))) Заходим в код и начинается праздник.

...if (replmsg.MsgName == «deal»)


у replmsg есть не только MsgName но и номер т.е. число и если учесть что примерно 25-30 млн. тиков проходит через этот метод то уже на этом этапе мы выходим из игры под названием «OsEngine HFT». 

… //- if id_ord_sell < id_ord_buy, то Side = buy/если id_ord_sell < id_ord_buy, то Операция = Купля
//- if id_ord_sell > id_ord_buy, то Side = sell/если id_ord_sell > id_ord_buy, то Операция = Продажа...



Смотрим дальше. Такой коментарий в коде в общем отражает суть но правильно использовать битовые маски для определения стороны покупка или продажа. Точно сейчас не помню вроде как это связано с тем что биржа(давно уже правда) для снижения нагрузки паралелила потоки где матчинг и(опять же если я не ошибаюсь) может возникнуть ситуация что утверждение что если id_ord_sell < id_ord_buy, то Операция = Купля будет не верным и в терминале будет покупка продажей и наоборот(проверить можно позвонив в техподдержку биржи и спросив(при условии что попадёте на кого надо)). В документации описано что означают битовые маски советую читать и развиваться(«ftp.moex.com/pub/ClientsAPI/Spectra/CGate/test/docs/p2gate_ru.pdf») стр. 52 

...ActiveSide 0x20000000000 Активная сторона в сделке. Заявка, приведшая к сделке при добавлении в стакан.
...PassiveSide 0x40000000000 Пассивная сторона в сделке. Заявка из стакана, участвующая в сделке.


Двигаемся дальше. Тут видно что в OsEngine пишут код очень весёлые ребята и любят конвертацию(прямо какая то маниакальная тяга к конвертации типов и работе со строками)

всё бы ничего только вот эта строка кажется как-то странно выглядит:



...trade.Price = Convert.ToDecimal(replmsg[«price»].asDecimal());

Как будто бы и так сгодилось:

...trade.Price = replmsg[«price»].asDecimal();



Коллеги которые пользуются софтом OsEngine сейчас должны хорошо взвесить все за и против т.к. просто взглянув на код тут такая дичь творится(а Алексей на голубом глазу шагает в колокацию за HFT как настоящий алгоспецназ(картинка помнится мне была такая лень искать ссылку но все наверное видели и так))

{
// ticks/тики
try
{
//- if id_ord_sell < id_ord_buy, то Side = buy/если id_ord_sell < id_ord_buy, то Операция = Купля
//- if id_ord_sell > id_ord_buy, то Side = sell/если id_ord_sell > id_ord_buy, то Операция = Продажа

byte isSystem = replmsg[«nosystem»].asByte();

// если сделка внесистемная, пропускаем ее
if (isSystem == 1)
{
return 0;
}

Trade trade = new Trade();
trade.Price = Convert.ToDecimal(replmsg[«price»].asDecimal());
trade.Id = replmsg[«id_deal»].asLong().ToString();
trade.Time = replmsg[«moment»].asDateTime();
trade.Volume = replmsg[«xamount»].asInt();
string securityNameCode = replmsg[«isin_id»].asInt().ToString();

...ToString()… Просто обожают коллеги работу со строками даже в «горячих потоках» что выглядит странно(почему? ответ на эту загадку в конце поста).

вот это иначе как извращение никак не назовёшь:
приходят данные(уже куча косяков которые оставили OsEngine далеко позади в плане реакции на событие) что делает код коллег он число конвертирует в строку(напомню что более 25 млн. тиков проходит эту и то что описано выше) и сравнивает


...
string securityNameCode = replmsg[«isin_id»].asInt().ToString();
...
Security security = _securities.Find(s => s.NameId == securityNameCode);

почему не сделать s.NameId числом чтобы просто получать и сравнивать а не заниматься постоянно конвертацией строк и их сравнением. Это же дикие тормоза для софта.

это
...
if (numberBuyOrder > numberSellOrder)
{
trade.Side = Side.Buy;
}
else
{
trade.Side = Side.Sell;
}...

надо сделать так

trade.Side = numberBuyOrder > numberSellOrder? Side.Buy: Side.Sell;

( в одну строку вместо 8)


portfolio.ValueBegin = Convert.ToDecimal(replmsg[«money_old»].asDecimal());
portfolio.ValueCurrent = Convert.ToDecimal(replmsg[«money_amount»].asDecimal());
portfolio.ValueBlocked = Convert.ToDecimal(replmsg[«money_blocked»].asDecimal());

зачем Convert.ToDecimal если оно уже возращает asDecimal? Добавьте ещё цикл и так раз 50 из одного типа в другой попереводите и когда эта весёлая игра закончится можно и поторговать дальше «OsEngine HFT». Почему сразу не сделать в portfolio у всех тип Decimal вместо того чтобы лишние вызовы делать?


Совет номер три.
— перечитайте книги по архитектуре ПО в местах где написано про «модель — вью — контроллер». У Вас в коде работа с данными ГУЯ напрямую. Даже если бы было через Invoke и даже если бы всё в одном потоке(а у Вас в отдельном) то всё равно чтобы данные были актуальными нужно использовать атомарные операции т.к. обычные переменные если их передавать на ГУЙ будут сильно(в контексте HFT) отличаться т.е. аск на ГУЕ и аск на бэкэнде это будут совсем разные аски(даже со всеми асинхронными «Invoke делами»). В с++ для того чтобы было видно между потоками есть atomic, volatile. В си шарп думаю тоже есть аналоги.


ну и напоследок так сказать «вишенка на торте» почему возможно OsEngine не сильно стремится к оптимизации кода и т.п. по этой ссылке(«github.com/AlexWan/OsEngine/blob/master/project/OsEngine/Robots/High%20Frequency/Fisher.cs») в коде есть метод который видимо отвечает за логику алгоритма. Там кроме прочего есть такой код:

...
private void CanselAllOrders()
{
    List<Position> openPositions = _tab.PositionsOpenAll;

    Position[] poses = openPositions.ToArray();

    for (int i = 0; poses != null && i < poses.Length;i++)
    {
        if(poses[i].State != PositionStateType.Open)
        {
            _tab.CloseAllOrderToPosition(poses[i]);
        }
        Thread.Sleep(200);
}
Thread.Sleep(1000);
...



думаю тут всё очевидно но позволю себе просмаковать этот момент. После такого кода вывеска «OsEngine HFT» достойна стать мемом коллеги.))) Слип есть даже в цикле.))) И в догонку при выходе из цикла ещё на секунду припаркуемся. «OsEngine HFT» ведь в колокации, стало быть торопиться некуда. Это наверное сделано для того чтобы с запасом(подождать ответов на транзакции). Но для понимания пока идёт это ожидание коллега из Владивостока уже сможет отправить заявки и получить на них ответ несколько кругов. Почему это делается тоже понять можно 7 000 000 кода с кучей преобразований(которые не нужны) в строку и обратно, работа со строками там где это противопоказано, кучей io bound операций, переключением контекста(там у них ведь потоков наверное ещё больше чем дублирования кода) и т.д. делает код дико тормозным и если не делать Thread.Sleep(1000) то терминал «замёрзнет» в ожидании пока процессор всё что написано сделает. Т.е. как бы ни собирался Алексей в походы за HFT в контексте лоу латенсей это никак не получится по определению, даже если поправит и сильно улучшит качество кода т.к. «за скоростью» надо идти «на легке» а не со всей приблудой да ещё так коряво написанной. Я когда писал коннектор к TWIME мог взять примеры на с++ на гитхаб но я этого не делал т.к. меня интересует не накопление чужого го.н.кода а качественный свой софт. Поэтому всегда сам и пишу. Например TWIME был написан с помощью снифера, блокнота, документации и с++ чтобы не было ничего лишнего, так дольше чем взять и переделать но зато я знаю каждый знак для чего нужен в своём коде.


Ладно удачи Алексею и пользователям его софта в HFT(присоединяюсь к его поздравлениям самим себе вот тут в конце поста(«smart-lab.ru/company/os_engine/blog/1178501.php»))

… Ещё раз поздравляю нас с выходом в зону HFT...



Ок.))) Жгите коллеги.


Но если честно немного жаль тех кто пользует OsEngine для того чтобы скорость, HFT и всё такое т.к. даже Thread.Sleep(1000) уже закрывает эту возможность напрочь. А ведь у кого-то могут быть действительно правильные мысли по поводу хфт но использование OsEngine делает невозможным это реализовать. Поэтому коллеги кто действительно хочет хфт пишите свой софт. Качество софта OsEngine относительно «накидываемых понтов» может вызывать только такие ассоциации.




Алготрейдинг. OsEngine покоряет новые высоты(только не будем уточнять откуда мерить).


Теперь буду ждать цикл постов о том как OsEngine делает рефакторинг кода. Это будет намного более увлекательней чем те которые были до этого. А может и не будет таких постов т.к. Алексей ведь больше про маркетинг чем про всё остальное.

29 Комментариев
  • Да, Sleep() это круто! Такого нет даже у меня в моем албанском коде.
  • ves2010
    11 июля 2025, 07:48
    на что только не пойдут люди не читавшие гост на оформление программной документации... 

    ну и критичный к скорости код я бы писал на си а не на с++…
  • Красаучег
    11 июля 2025, 09:03
    __rtx, лучше не смотри!
  • websan
    11 июля 2025, 10:46
    Если вы написали действительно работящего робота на рынке, стали бы продавать за 28 000 рублей?))

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн