Блог им. __rtx

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

    • 11 июля 2025, 07:20
    • |
    • __rtx
  • Еще

Алексей продолжает серию юмористических постов о том как они идут в колокацию и 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 делает рефакторинг кода. Это будет намного более увлекательней чем те которые были до этого. А может и не будет таких постов т.к. Алексей ведь больше про маркетинг чем про всё остальное.

  • обсудить на форуме:
  • OsEngine
1.6К | ★2
29 комментариев
Да, Sleep() это круто! Такого нет даже у меня в моем албанском коде.
на что только не пойдут люди не читавшие гост на оформление программной документации... 

ну и критичный к скорости код я бы писал на си а не на с++…
avatar
ves2010, а Вы какой ГОСТ имеете в виду? Можете номер указать?
avatar

Andy, Единая система программной документации — Википедия

там их много… но знать нужно все... 
вообще я видел только одну контору которая соблюдала этот гост... 

помню пришел в одну контору а там прогеры чтоб их не уволили стерли все коменты и из документации помимо 60к строк кода на ассемблере была блок схема на формате а1 желтая от времени в пятнах от пролитого кофе порванная на складках и подклеенная изнутри газеткой аиф… это прям был как священный артефакт...

я начальству прямо задал вопрос а где все остальное… вообщем не взяли мя на работу... 

avatar
ves2010, ГОСТЫ 79года. Они уже мало связаны с текущей действительностью.
avatar
ves2010, OsEngine конечно дичь, но то что вы советуете — еще более дичь.

Критичный к скорости код следует писать прежде всего понимая, что делаешь, а не наоборот.
__rtx, лучше не смотри!
avatar
Если вы написали действительно работящего робота на рынке, стали бы продавать за 28 000 рублей?))
avatar
через год-два в Cursor прогонят через gpt и норм станет )
avatar
Я подумал, откуда у мертвого проекта столько комментариев под новостью. Оказывается, не автор написал.

Да проекту трындец. ИИ окончательно обнулил все эти усилия. Кто в здравом уме теперь будет вибирать что-то такое, когда ИИ пишет и коннектор, и стратегию, и даже тесты выдает. Как раз самое то, кто в поисках граалей в готовом коде. Основная категория — кто не знает даже что такое компиляция — уходит от таких проектов к Соннету и ГПТ. Этот тренд уже даже не в трейдинге. Он вообще в целом. Видно затухание активности Опен Сорса. ИИ бьет по SOE и самой парадигме коворкинга в опен сорс репах.

Я правильно понял, что у них там общение между потоками через простые переменные? О сколько нам открытий чудных...

std::atomic<> кстати не поможет, тем более volatile (он вообще для другого).

художника может каждый обидеть. 
avatar
__rtx, помимо атомарности изменений, есть еще проблема с порядком видимости. Если вся передаваемая информация укладывается в одну переменную — ок. Если переменных несколько — до получателя в другом потоке модификации могут дойти не в той последовательности, как их менял отправитель. 
Каждый пилит как хочет. Кому надо — посмотрит роундтрипы и сделает выводы. А вот обилие постов от Лехи с подчеркиванием исключительной офигенности продукта в каждом и хуллиард бесплатных роботов — однозначный маркер, что трогать это нет смысла.
Слипы — прикольно )
avatar
Норм, на рынке нужны медленные алгоритмы, которые считают себя быстрыми — другим на радость )
С пенальти странно, конечно. Можно просто добавить пенальти к now и проверять на входе без блокировок.
avatar
__rtx, любопытно, как с такими слипами софт проходит сертификацию и получает допуск. За секунду пенальти там может нехилая пачка скопиться.
avatar

Читайте на SMART-LAB:
Фото
Фунт как новый защитный актив? Бюджет Ривз и PMI поддерживают ралли GBP
EURUSD в пятницу «паркуется» перед окончанием недели около 1,1650: пара отдала часть роста, но попытки продавить её вниз быстро выкупаются....
Фото
Угадайте компанию, которая готовит первый выпуск облигаций на Мосбирже
Подсказки: 📍 быстро закрывает вопросы с просроченными задолженностями; 📍 работает строго в рамках российского законодательства и ФЗ-230; 📍...
Фото
Число инвесторов RENI достигло 100 тысяч человек
Получили свежий отчет Московской Биржи. Количество наших инвесторов выросло на 4 тыс. до 100 тыс. человек, +62% с начала года. Средний размер...

теги блога __rtx

....все тэги



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