← 返回

Список задач

И так, предлагаю подытожить то, что нужно сделать и в каком порядке.

Задачи сгруппировал по группам (копировал из соседней ветки http://stocksharp.com/forum/1275/&p=2). Первая группа - самая приоритетная. Без нее делать задачи из следующей бессмысленно. Сами задачи внутри этой группы так же выстроены по очередности.

Базовая группа:

  1. Описать основные метаданные, как это сделано для Квика (DdeSecurityColumns, DdeTradeColumns и т.д.). Я это уже показал на примере класса PlazaFutureColumns, но там колонки не реальные. Предлагаю прямо разобрать по названию таблиц потоков, кто за какие будет отвечать (описывать в коде). Таблицы только основные: инструменты, заявки, сделки и т.д.
  2. Подписываться на произвольные потоки + отписываться.
  3. Получать стаканы. Производная от предыдущей задача. Выделил ее отдельно.
  4. Заявки (регистрация, снятие и перемещение). Я уже сделал через класс Message заполнение полей для отправки транзакций. Это дело надо доделать.
  5. Парсинг ответа от транзакций (внимание! всех транзакций, а не только, что в предыдущем пункте). Я не знаю, в каком виде они приходят, но могу сказать, какой результат должен быть. Это должно быть PlazaException с кодом ошибки (чтобы не мучится сравнение строчек в коде). И код не ввиде числа (что не так уж лучше строчки), а нормального перечисления (enum).

Первую задачу предлагаю сделать сообща. Остальные - разбираем по тем признакам, кто какие таблицы описывал (например, кто описал стакан, тот занимается задачей 3, кто инструменты - задачей 2, кто заявки - задачей 4).

Второстепенная группа:

  1. Дописать все остальные метаданные (позиции, счета, волатильность, маржа, клиринг, доп информация о деривативах, индексы).
  2. Фильтрация потоков. Так как Плаза не дает фильтр (камрад skuvv написал об этом), но в клиентском коде прописывать фильтры дело не благодарное, предлагаю это симулировать через RegisterXXX + UnRegisterXXX.
  3. Возможность задавать глубину стакана (как я понял, это влияет на то, к какому потоку необходимо цепляться).
  4. Поддержка агрегированного стакана и обычного. Для этого можно использовать GroupedQuote (внутри себя он будет содержать не агрегированные котировки).

Третьестепенная группа:

  1. С помощью метаданных научиться строить конфиги ini. Как вариант, через PlazaTable (куда собственно и будут добавляться колонки из пункта 1 пред группы). Сейчас ini файлы программно редактировать нельзя. И если роботу нужны спец колонки нужно менять формат ini схем. Я предлагаю до загрузки этих схем давать возможность менять из программно (парсить и менять ini файлы на лету). + как фича автоматически сканировать директорию при старте и создавать с правильным набором колонок сами PlazaTable. Возможно, здесь поможет TableSet.
  2. Время биржи.
  3. Все остальные транзакции (FutChangeClientMoney, OptChangeExpiration и т.д.).
  4. Составные инструменты.
  5. На форуме доступна x64 версия. Нужна прозрачная поддержка (без перекомпиляции) x86 и x64.
  6. Обертка над роутером (чтобы так же, не ручками править конфиги, а программно).
  7. Документация (как xml, так и обычная). Если нужно сделать как у S#, то необходимо использовать Sandcastle.

Особенности в работе.

Старайтесь писать так, как это принято в .NET и в частности C# коде. Публичные методы классы и т.д. описывайте xml комментариями (лучше не сочинять, а копировать из документации Плазы, чтобы быть не с планеты Юпитер, а ближе к тем, кто будет это использовать и вторым глазом подглядывать в документацию). Предлагаю использовать R# как средство для контроля качества кода. Пишите юнит тесты (заведите отдельный проект в sln).

评论 (78)

登录创建账户, 登录或注册以发表评论

nlrf
nlrf

А где посмотреть PlazaFutureColumns? Всех с Новым Годом!

Mikhail Sukhov
Mikhail Sukhov 作者

nlrf: А где посмотреть PlazaFutureColumns? Всех с Новым Годом!

Приехали. Я же доступ дал к ТФС и Вы написали, что успешно подключились. Там лежит то, что я начинал по Плазе. В том числе и данный класс.

nlrf
nlrf

Михаил, на эту тему у нас уже была переписка. Я: "Но тут новая проблемка нарисовалась. В студии командный проект (папка 1.0) пустым выглядит. Встроен ли Teamprise Explorer в студию 2010, или его надо отдельно скачать и инсталлировать? Если первое, то как он называется в русскоязычной версии? Если же это и есть командный обозреватель в новой версии безо всяких рычагов, то в нём-то и пусто". Вы: "Не туда смотрите. Исходники в другом месте". В итоге я смотрю: 1) не так; 2) не там; или 3) не то?

Mikhail Sukhov
Mikhail Sukhov 作者

nlrf: Михаил, на эту тему у нас уже была переписка. Я: "Но тут новая проблемка нарисовалась. В студии командный проект (папка 1.0) пустым выглядит. Встроен ли Teamprise Explorer в студию 2010, или его надо отдельно скачать и инсталлировать? Если первое, то как он называется в русскоязычной версии? Если же это и есть командный обозреватель в новой версии безо всяких рычагов, то в нём-то и пусто". Вы: "Не туда смотрите. Исходники в другом месте". В итоге я смотрю: 1) не так; 2) не там; или 3) не то?

Я в инструкции давал ссылку на доку по TFS. Прочтите, там не много, лишним не будет.

aspirant
aspirant

Как будем сообщать о загрузке файлов, выполнении задач? Только что загрузил три файла, связанные с настройкой конфиг-файла роутера.

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Как будем сообщать о загрузке файлов, выполнении задач? Только что загрузил три файла, связанные с настройкой конфиг-файла роутера.

Комменты в самом TFS + отпишите на форум.

Mikhail Sukhov
Mikhail Sukhov 作者

И так, праздники прошли, логинов роздано с двух десятков, задачи есть, план по задачам есть (по крайней мере никто не высказывался против, впрочем как и за). Предлагаю разбирать задачи. Как я уже написал, первую задачу можно сделать сообща. Это будет своеобразной пионерской задачей и сразу покажем, как нужно координировать действия.

Трублю всеобщий сбор.

aspirant
aspirant

Предлагаю взять Поток FORTS_OPTTRADE_REPL - Опционы: заявки и сделки

Таблицы: • orders_log - Журнал заявок • deal - Журнал сделок • heartbeat - Служебная таблица cерверных часов

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Предлагаю взять Поток FORTS_OPTTRADE_REPL - Опционы: заявки и сделки

Таблицы: • orders_log - Журнал заявок • deal - Журнал сделок • heartbeat - Служебная таблица cерверных часов

Я беру FORTS_FUTINFO_REPL - Фьючерсы: справочная и сессионная информация. Только то, что относиться к инструментам и сессиям:

  • fut_vcb
  • session
  • fut_sess_contents
  • fut_instruments
Mikhail Sukhov
Mikhail Sukhov 作者

Так, а что с остальными? Нужно как в школе называть всех по списку?

Mikhail Sukhov
Mikhail Sukhov 作者

Я сделал несколько чекинов, чтобы структурировать первоначальное (+ сделать компилируюемым).

lkor
lkor

если всё свободно то FORTS_FUTTRADE_REPL - Фьючерсы: заявки и сделки

Таблицы: orders_log - Журнал заявок deal - Журнал сделок multileg_orders_log - Журнал заявок по связкам multileg_deal - Журнал сделок по связкам heartbeat - Служебная таблица cерверных часов

я так понимаю orders_log, deal и heartbeat первоочередные, остальные могут потерпеть.

Mikhail Sukhov
Mikhail Sukhov 作者

lkor: если всё свободно то FORTS_FUTTRADE_REPL - Фьючерсы: заявки и сделки

Таблицы: orders_log - Журнал заявок deal - Журнал сделок multileg_orders_log - Журнал заявок по связкам multileg_deal - Журнал сделок по связкам heartbeat - Служебная таблица cерверных часов

я так понимаю orders_log, deal и heartbeat первоочередные, остальные могут потерпеть.

По мне так и multi может подождать. Но если есть желание все сделать сразу, почему бы нет.

aspirant
aspirant

Михаил,

Прочитав ваше сообщение, понял, что мне нужно начать с

Поток FORTS_OPTINFO_REPL - Опционы: справочная и сессионная информация Схема данных Таблицы: • opt_exp_orders - Заявки на экспирацию • opt_vcb - Справочник торгуемых активов • opt_sess_contents - Cправочник торгуемых инструментов • opt_sess_settl - Результаты клиринга: волатильность и теоретические цены

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Михаил,

Прочитав ваше сообщение, понял, что мне нужно начать с

Поток FORTS_OPTINFO_REPL - Опционы: справочная и

Ок, тогда буду смотреть на ваши чекины, так как у нас могут метаданные пересекаться. Например, вряд ли опционная сессия чем-то отличается от фьючерсной. Давайте сделаем так. Все, что вы думаете относиться к деривативам вообще (фьючи опшины) - выносите не в класс PlazaOptionЧтотоТам, а в PlazaDerivativeЧтотоТам (если его еще нет, то надо создать).

aspirant
aspirant

Михаил,

Создал класс PlazaDerivativeColumns, который будет использоваться для справочников базовых контрактов для инструментов (fut_vcb, opt_vcb).

Вопрос по реализации: сейчас в классе PlazaFutureColumns колонки - это открытые статические переменные для чтения, а, например, в классе DdeSecurityColumns колонки - открытые статические свойства только для чтения. Прописываем колонки, как переменные? Интереса ради, с чем связано такое изменение?

aspirant
aspirant

Добавил класс PlazaDerivativeColumns

Описывает колонки справочники базовых контрактов для инструментов Поток FORTS_FUTINFO_REPL - таблица fut_vcb Поток FORTS_OPTINFO_REPL - таблица opt_vcb

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Добавил класс PlazaDerivativeColumns

Описывает колонки справочники базовых контрактов для инструментов Поток FORTS_FUTINFO_REPL - таблица fut_vcb Поток FORTS_OPTINFO_REPL - таблица opt_vcb

Просьба указывать комментарий при чекине. Есть такое поле вверху в окне Checkin changed.

Mikhail Sukhov
Mikhail Sukhov 作者

Mikhail Sukhov:

aspirant: Добавил класс PlazaDerivativeColumns

Описывает колонки справочники базовых контрактов для инструментов Поток FORTS_FUTINFO_REPL - таблица fut_vcb Поток FORTS_OPTINFO_REPL - таблица opt_vcb

Просьба указывать комментарий при чекине. Есть такое поле вверху в окне Checkin changed.

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

Сделал разъединение PlazaDerivativeColumns на PlazaFutureColumns и PlazaOptionColumns. PlazaDerivativeColumns теперь абстрактный, и статические поля убраны. Тоесть, вместо написания myTable.Columns.Add(PlazaDerivativeColumns.ClientCode) теперь будет myTable.Columns.Add(PlazaColumns.Futures.ClientCode).

Вопрос. А почему классы в проектный файл csproj не добавляете?

skuvv
skuvv

добавил таблицу Common для FORTS_FUTCOMMON_REPL ps таблица совпадает с таблицей их потока FORTS_OPTCOMMON_REPL

  • беру FORTS_POS_REPL,FORTS_PART_REPL,RTS_INDEX_REPL,FORTS_VM_REPL
Mikhail Sukhov
Mikhail Sukhov 作者

skuvv: добавил таблицу Common для FORTS_FUTCOMMON_REPL ps таблица совпадает с таблицей их потока FORTS_OPTCOMMON_REPL

  • беру FORTS_POS_REPL,FORTS_PART_REPL,RTS_INDEX_REPL,FORTS_VM_REPL

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

aspirant
aspirant

Понял, приму к сведению.

lkor
lkor

добавил таблицу orders_log из FORTS_FUTTRADE_REPL. наследовал от PlazaColumns

PS Таблица совпадает с orders_log из FORTS_OPTTRADE_REPL (отличие в том что для опционов поле status может принимать ещё 2 дополнительных значения)

PPS Первый опыт работы в команде и с TFS - ногами не бить.🙄

PPPS как изменить платформу с 4.0 на 3.5? Найти в VS2010 у меня не получилось. 🤨

aspirant
aspirant

lkor: PPPS как изменить платформу с 4.0 на 3.5? Найти в VS2010 у меня не получилось. 🤨

В свойствах проекта на закладе Приложение / Application

lkor
lkor

aspirant:

lkor: PPPS как изменить платформу с 4.0 на 3.5? Найти в VS2010 у меня не получилось. 🤨

В свойствах проекта на закладе Приложение / Application

Спасибо!

aspirant
aspirant

Описал метаданные по опционам. Вопрос к знающим: нужно описывать таблицу opt_rejected_orders - Отвергнутые в клиринг заявки?

lkor
lkor

добавил таблицу deal из FORTS_FUTTRADE_REPL

PS Таблица совпадает с deal из FORTS_OPTTRADE_REPL (опять отличие в разных вариантах значений поля status)

lkor
lkor

таблица heartbeat из FORTS_FUTTRADE_REPL уже существует в виде PlazaHeartBeatColumns, собственно её не описываю.

добавил таблицы multileg_deal и multileg_orders_log из FORTS_FUTTRADE_REPL

Mikhail Sukhov
Mikhail Sukhov 作者

Надо бы произвести уже сейчас чистку некоторых вещей. Отпишу здесь по названию классов, кто за них ответственен - разбирайте. Но прочитайте все пункты, так как может быть и у вас так же.

PlazaFutCommonColumns:

  1. Наследования от базового класса PlazaColumns отсутствует.
  2. Cо статических полей переделать на обычные (иначе наследование работать не будет), и записать свою таблицу в PlazaColumns как статическое поле.
  3. Придерживаться .NET стиля. Тоесть, не Sess_id, а SessId, не status а Status.
  4. Если таблица одинаковая для опцов и фьючей, то и называть класс нужно Derivative, а не Futute. Как плюс, задел на будущее, что РТС расширит поля в одной из таблиц, и тогда они перестанут быть эквивалентными. Тогда можно будет сделать Derivative абстрактным и сделать два специфичных наследника.
  5. Таблицы FORTS_FUTCOMMON_REPL и FORTS_OPTCOMMON_REPL не одинаковы. Это значит что уже сейчас надо выносить в базовый класс общие поля и производить наследование. Особенно, обратить внимание на передачу типа таблицы в базовый класс. Как пример, PlazaDerivativeColumns и PlazaOptionColumns.

PlazaFutTradeDealColumns, PlazaFutTradeMultilegDealColumns, PlazaFutTradeMultiLegOrdersLogColumns, PlazaFutTradeOrdersLogColumns:

  1. Тоже самое, что и PlazaFutCommonColumns, кроме наследования от PlazaColumns.
  2. Везде указывается тип таблицы Trade. Так ли это?

PlazaHeartBeatColumns

  1. Неправильный тип таблицы у ServerTime.
  2. Это единственная таблица с системными данными? Если нет, то название System не подходит.
  3. Конструкторы у классов с метаданными лучше делать internal.

Насчет общего стиля именования. Предлагаю, писать полностью имена, тоесть:

OrdId -> OrderId SessId -> SessionId Dir -> Direction

Название ISIN можно писать как ISIN, Isin, но никак не IsIn. Аббревиатуры (в данном случае International Securities Identification Number) так не пишутся.

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

skuvv
skuvv

Mikhail Sukhov: Насчет общего стиля именования. Предлагаю, писать полностью имена, тоесть:

OrdId -> OrderId SessId -> SessionId Dir -> Direction

Название ISIN можно писать как ISIN, Isin, но никак не IsIn. Аббревиатуры (в данном случае International Securities Identification Number) так не пишутся.

Предлагаю не писать тип данных в комментариях к поля (i) (t) и т.д.. Добавляет лишнюю смысловую нагрузку, которая по сути вряд ли будет использоваться из вне. А есть ли смысл менять названия полей? Поидее по документам plaza будет легче сопостовлять поля если они будут с оригинальными совпадать...

Mikhail Sukhov
Mikhail Sukhov 作者

skuvv: А есть ли смысл менять названия полей? Поидее по документам plaza будет легче сопостовлять поля если они будут с оригинальными совпадать...

Названия я предлагаю не менять, а дописывать до полного... В любом случае, будет много, что не как в стандартном АПИ. Но нужно одновременно сделать удобным и работу с документацией Plaza, и дальнейшей работой с роботом. Мне кажется, что документация по Плазе нужна только в первое время. В дальнейшем видеть в коде названия ordid sessid и т.д. не очень приятно. Но это только мое мнение.

aspirant
aspirant

Mikhail Sukhov: PlazaHeartBeatColumns

  1. Неправильный тип таблицы у ServerTime.
  1. Это единственная таблица с системными данными? Если нет, то название System не подходит.
  1. Конструкторы у классов с метаданными лучше делать internal.

исправил

Mikhail Sukhov: Насчет общего стиля именования. Предлагаю, писать полностью имена, тоесть:

OrdId -> OrderId SessId -> SessionId Dir -> Direction

Название ISIN можно писать как ISIN, Isin, но никак не IsIn. Аббревиатуры (в данном случае International Securities Identification Number) так не пишутся.

Изначально при именовании переменных я хотел сохранить близость к оригиналу, на случай если кому-то придется работать с документацией самой Плазы. Сейчас понимаю, что читаемость кода, пожалуй, важнее. Тем более что я сам себе противоречил, обозвав классы полными именами, например, PlazaOptionSessionContentsColumns, а не PlazaOptSessContentsColumns.

Насчет IsIn'а я ступил. Предлагаю Isin, потому что, во-первых, кроме Isin'а еще будет переменная IsinId, а во-вторых, это в духе .Net'а, см. System.Xml.

Mikhail Sukhov: Предлагаю не писать тип данных в комментариях к поля (i) (t) и т.д.. Добавляет лишнюю смысловую нагрузку, которая по сути вряд ли будет использоваться из вне.

Пожалуй, это действительно был перебор, сейчас исправлю

lkor
lkor

Mikhail Sukhov: PlazaFutCommonColumns:

  1. Наследования от базового класса PlazaColumns отсутствует.
  1. Cо статических полей переделать на обычные (иначе наследование работать не будет), и записать свою таблицу в PlazaColumns как статическое поле.
  1. Придерживаться .NET стиля. Тоесть, не Sess_id, а SessId, не status а Status.
  1. Если таблица одинаковая для опцов и фьючей, то и называть класс нужно Derivative, а не Futute. Как плюс, задел на будущее, что РТС расширит поля в одной из таблиц, и тогда они перестанут быть эквивалентными. Тогда можно будет сделать Derivative абстрактным и сделать два специфичных наследника.
  1. Таблицы FORTS_FUTCOMMON_REPL и FORTS_OPTCOMMON_REPL не одинаковы. Это значит что уже сейчас надо выносить в базовый класс общие поля и производить наследование. Особенно, обратить внимание на передачу типа таблицы в базовый класс. Как пример, PlazaDerivativeColumns и PlazaOptionColumns.

PlazaFutTradeDealColumns, PlazaFutTradeMultilegDealColumns, PlazaFutTradeMultiLegOrdersLogColumns, PlazaFutTradeOrdersLogColumns:

  1. Тоже самое, что и PlazaFutCommonColumns, кроме наследования от PlazaColumns.
  1. Везде указывается тип таблицы Trade. Так ли это?
  1. привёл все поля к .NET стилю
  2. со статических переделал на обычные
  3. убрал из комментариев тип поля в Плазе ( (с7), (i4) и т.д. )
  4. для Plaza*OrdersLogColumns изменил тип таблицы на Order
  5. переимевал PlazaFutTradeDealColumns в PlazaDerivativeTradeDealColumns, PlazaFutTradeOrdersLogColumns в PlazaDerivativeTradeOrdersLogColumns
  6. добавил соответсвующие поля в PlazaColumns для таблиц из FORTS_FUTTRADE_REPL. для таблиц из FORTS_OPTTRADE_REPL поля добавил и закоментировал, ибо не мой это поток. не надо - удалим.

вроде всё.

aspirant
aspirant

Все исправил. Не знаю только, как правильно именовать:

/// <summary> /// Базовое ГО под одну покрытую позицию подписчика (руб) /// </summary> public readonly PlazaColumn bgo_c = new PlazaColumn(PlazaTableTypes.Option, "bgo_c", typeof(decimal), true);

/// <summary> /// Базовое ГО под одну непокрытую позицию подписчика (руб) /// </summary> public readonly PlazaColumn bgo_nc = new PlazaColumn(PlazaTableTypes.Option, "bgo_nc", typeof(decimal), true);

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Все исправил. Не знаю только, как правильно именовать:

///

/// Базовое ГО под одну покрытую позицию подписчика (руб) /// public readonly PlazaColumn bgo_c = new PlazaColumn(PlazaTableTypes.Option, "bgo_c", typeof(decimal), true);

///

/// Базовое ГО под одну непокрытую позицию подписчика (руб) /// public readonly PlazaColumn bgo_nc = new PlazaColumn(PlazaTableTypes.Option, "bgo_nc", typeof(decimal), true);

Главное, не с маленькой буквы, и_убрать_подчеркивания. BgoC, BgoNc. Вполне сойдет.

aspirant
aspirant

Еще вопрос. Что делать с этими переменными?

/// <summary> /// Дата окончания обращения инструмента /// </summary> public readonly PlazaColumn DPG = new PlazaColumn(PlazaTableTypes.Option, "d_pg", typeof(DateTime), true);

/// <summary> /// Дата начала экспирации инструмента /// </summary> public readonly PlazaColumn D_Exec_Beg = new PlazaColumn(PlazaTableTypes.Option, "d_exec_beg", typeof(DateTime), true);

/// <summary> /// Дата окончания экспирации инструмента /// </summary> public readonly PlazaColumn D_Exec_End = new PlazaColumn(PlazaTableTypes.Option, "d_exec_end", typeof(DateTime), true);

Решарпить плазовские названия? Или писать что-то типа:

d_pg: ExerciseDate d_exec_beg: ExpirationStartDate d_exec_end: ExpirationEndDate

Если мы за читаемость, я все-таки предлагаю второй вариант

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: d_pg d_exec_beg d_exec_end

Как они сами в таком ужасе разбираются?

aspirant: Если мы за читаемость, я все-таки предлагаю второй вариант

Согласен.

aspirant
aspirant

Mikhail Sukhov:

aspirant: d_pg d_exec_beg d_exec_end

Как они сами в таком ужасе разбираются?

aspirant: Если мы за читаемость, я все-таки предлагаю второй вариант

Согласен.

Готово

Mikhail Sukhov
Mikhail Sukhov 作者

Я так понимаю, задачу 1 не только выполнили, но и перевыполнили? Тогда корректируем задачи в зависимости от силы исполнения. Те, кто хочет остаться на метаданных, пусть дописывает оставшиеся. Только перед этим надо написать здесь, какие именно таблицы хочется дописать, чтобы не получилось дублирования работы.

Далее, задачи с 2 по 5. Задачи 2 и 3 мне кажется схожи. Поэтому, предлагаю стаканы отложить до тех пор, пока не будет закончена задача 2. Если так окажется, что желающих больше чем задач (вряд ли, но все же), то предлагаю спуститься к задачам из третьестепенной группы (второстепенные задачи слишком тесно связаны с первостепенными).

Схема стандартная - пишем кто-что хочет делать и делаем.

aspirant
aspirant

В принципе по опционам за исключением стаканов описал все. Хотел бы перейти к п. 2

lkor
lkor

смотрю в сторону п.4

Mikhail Sukhov
Mikhail Sukhov 作者

lkor: смотрю в сторону п.4

Тогда я п5.

skuvv
skuvv

Буду доделывать метаданные... Добавил FORTS_OPTORDERBOOK_REPL и FORTS_FUTORDERBOOK_REPL. update: FORTS_FUTTRADE_REPL - orders_log FORTS_FUTTRADE_REPL - multileg_orders_log FORTS_OPTTRADE_REPL - orders_log Привел к использованию одного базового класса. Есть косяк почти во всех мета - типа поля i8, это не int, a int64, за 32бита перевалило еще в декабре... Есть предложение:

  1. вынести все мета-файлы в отдельное место/папку для убоства навигации
  2. придерживаться для мета-файлов общего наименования типа: Plaza-RerlicationName-Type-TableName-Columns, например PlazaOrderBookFutureOrdersColumns
Mikhail Sukhov
Mikhail Sukhov 作者

skuvv: Есть косяк почти во всех мета - типа поля i8, это не int, a int64, за 32бита перевалило еще в декабре...

А в чем косяк? На C# есть эквивалент - long.

skuvv: Есть предложение:

  1. вынести все мета-файлы в отдельное место/папку для убоства навигации
  2. придерживаться для мета-файлов общего наименования типа: Plaza-RerlicationName-Type-TableName-Columns, например PlazaOrderBookFutureOrdersColumns
  1. Поддерживаю.
  2. Я бы сделал PlazaFutureOrderBookOrdersColumns, так как это ближе к спецификации Плазы.

Не забывайте про комментарии в коммите.

aspirant
aspirant

Mikhail Sukhov:

skuvv: Есть косяк почти во всех мета - типа поля i8, это не int, a int64, за 32бита перевалило еще в декабре...

А в чем косяк? На C# есть эквивалент - long.

Я везде указывал эти данные (i8), как int. Буду исправлять.

aspirant
aspirant

Mikhail Sukhov: Я бы сделал PlazaFutureOrderBookOrdersColumns, так как это ближе к спецификации Плазы.

Поддерживаю.

aspirant
aspirant

Михаил,

Только что добавил класс PlazaStream. Это пока сырой вариант, потому что есть несколько неразрешенных вопросов. Поскольку логики много, я выделил класс в отдельный файл (я знаю, что вы уже создали приватный класс DerivativeStreams). Потом окончательную версию можно будет поместить, куда нужно.

Сначала комментарий: реализация алгоритма получения данных (метод Run) навеян примером BaselessClient, который шел вместе с тестовой версией Плазы.

Предложения:

  • Предлагаю из названий потоков сделать перечисление PlazaStreamName. Могу взять на себя. В файле с классом уже есть заготовка.
  • То же самое относится к схемам репликации (перечисление PlazaReplicationSchemeName).
  • Название таблицы пока передается как строковая переменная. В принципе могу свести все названия в одно перечисление. Пока не знаю, нужно ли? Все эти три значения нужны для подписи к потокам

Вопросы:

  1. Какого типа исключений нужно генерить? PlazaException - это для внешних/сторонних пользователей или для кода внутри проекта мы их тоже выкидываем?
  2. Какие классы внутри проекта будут пользователем класса PlazaStream и в каком виде им нужно выдавать данные (по одной записи или сразу массив записей и т.д.)? Данные приходят в событиях StreamDataInserted / StreamDataDeleted, где есть параметр типа CP2Record, который содержит значения одной записи таблицы.
Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Михаил,

Только что добавил класс PlazaStream. Это пока сырой вариант, потому что есть несколько неразрешенных вопросов. Поскольку логики много, я выделил класс в отдельный файл (я знаю, что вы уже создали приватный класс DerivativeStreams). Потом окончательную версию можно будет поместить, куда нужно.

DerivativeStreams мне кажется подобием PlazaColumns, тоесть контейнер всех доступных стримов. Так что в логике пересечения не должно быть.

aspirant: Сначала комментарий: реализация алгоритма получения данных (метод Run) навеян примером BaselessClient, который шел вместе с тестовой версией Плазы.

Мы уже убедились, какого уровня программисты работают в РТС. Так что не стоит равняться на них, даже если речь идет о примерах.😀

Что касается реализации, то могу сказать следующее. PlazaStream не должен содержать логики по управлению потоком (thread, и далее по тексту, чтобы не была путаница с потоками данных РТС, которые stream). В нем вообще не должно быть создание никаких потоков. В идеале у него должны быть пару методов Open Close, и события о получении новых данных, удалении, ошибке и т.д. А вот уже управлять стримами (в том числе и создавать единый поток для обработки всех стримов) нужно в PlazaTrader.

aspirant: Предложения:

  • Предлагаю из названий потоков сделать перечисление PlazaStreamName. Могу взять на себя. В файле с классом уже есть заготовка.
  • То же самое относится к схемам репликации (перечисление PlazaReplicationSchemeName).
  • Название таблицы пока передается как строковая переменная. В принципе могу свести все названия в одно перечисление. Пока не знаю, нужно ли?

Все эти три значения нужны для подписи к потокам

  1. PlazaStreamName. Енумераторы обычно окончиваются на множественное число. Плюс по логике это никакие не Name (енумы не могут содержать стринги). Я бы назвал PlazaStreams.
  2. PlazaReplicationSchemes🙄
  3. А вот тут самое интересно. А для чего собственно писали PlazaTable и мучались с метаданными? Надо использовать по максимуму ООП. Я бы передавал в стрим PlazaTable.
  1. Такие вещи как PlazaStream неплохо бы делать IDisposable. Советую наследоваться от Disposable (Ecng.Common), он по-функциональнее.
  2. Зачем стриму знать, в каком он конфиге? За это должен отвечать соответствующий TableConfigParser. И он же так же должен принимать на вход PlazaTable.

aspirant: Вопросы:

  1. Какого типа исключений нужно генерить? PlazaException - это для внешних/сторонних пользователей или для кода внутри проекта мы их тоже выкидываем?
  1. Какие классы внутри проекта будут пользователем класса PlazaStream и в каком виде им нужно выдавать данные (по одной записи или сразу массив записей и т.д.)? Данные приходят в событиях StreamDataInserted / StreamDataDeleted, где есть параметр типа CP2Record, который содержит значения одной записи таблицы.

  2. Лучше стандартные, которые в .NET. Честно говоря я не помню случаем, когда бы использовал другие исключения, вместо ArgExp, ArgOutExp, ArgNullExp, InvOptExp. Все остальные какие-то натянутые, и далекие от простых будней. Разделения на внешний и внутренний не понял.

  3. А вот тут не могу сказать. Как удобнее, так и надо. Тоесть Плаза за раз отсылает только одну строчку?

aspirant
aspirant

Mikhail Sukhov: Что касается реализации, то могу сказать следующее. PlazaStream не должен содержать логики по управлению потоком (thread, и далее по тексту, чтобы не была путаница с потоками данных РТС, которые stream). В нем вообще не должно быть создание никаких потоков. В идеале у него должны быть пару методов Open Close, и события о получении новых данных, удалении, ошибке и т.д. А вот уже управлять стримами (в том числе и создавать единый поток для обработки всех стримов) нужно в PlazaTrader.

Дело в том, что работа с плазовским стримом построена по принципу windows message loop: в неком методе программа вызывает метод CP2ConnectionClass.ProcessMessage(out cookie, PollInterval) в бесконечном цикле, а вся информация приходит через события. Если выводить создание и управление thread'ами, это нужно делать в отдельном классе. Иначе, если придется подключаться к стримам в нескольких местах, придется дублировать много кода.

Mikhail Sukhov:

  1. PlazaStreamName. Енумераторы обычно окончиваются на множественное число. Плюс по логике это никакие не Name (енумы не могут содержать стринги). Я бы назвал PlazaStreams.
  2. PlazaReplicationSchemes

Насчет Name вы правы - уберу. По поводу множественного числа: рекомендация MS - енумераторы в множественном числе, только если они битовые флаги, поэтому все-таки настаиваю на единственном.

Mikhail Sukhov: 3. А вот тут самое интересно. А для чего собственно писали PlazaTable и мучались с метаданными? Надо использовать по максимуму ООП. Я бы передавал в стрим PlazaTable.

Я помню о настраиваемости. Пока в черновом варианте я передаю путь к файлу, чтобы сразу можно было протестировать логику подключения, опроса, получения сообщений и т.д.
С метаданными такая вещь: стримы в Плазе настраиваются из файлов, даже по умолчанию (методы TableSet.InitFromIni / TableSet.InitFromIni2). Если передавать классу PlazaStream в конструкторе PlazaTable, тогда внутри PlazaStream некий класс TableConfigParser должен будет генерить файл с нужной схемой, чтобы передать ее TableSet.InitFromIni. Здесь такой момент: предлагаю в SpecialFolder.ApplicationData создать путь Stocksharp\PlazaTrader\Scheme, в котором хранить свои конфиг-схемы-файлы или их создавать в папке TEMP. Создавать их в подпапке самой Плазы не стоит: требуется админский доступ.

Mikhail Sukhov: 4. Такие вещи как PlazaStream неплохо бы делать IDisposable. Советую наследоваться от Disposable (Ecng.Common), он по-функциональнее.

Сделаю

Mikhail Sukhov:

  1. Лучше стандартные, которые в .NET. Честно говоря я не помню случаем, когда бы использовал другие исключения, вместо ArgExp, ArgOutExp, ArgNullExp, InvOptExp. Все остальные какие-то натянутые, и далекие от простых будней. Разделения на внешний и внутренний не понял.

Чтобы понимать в будущем, в каких случаях мы выкидываем PlazaException?

Mikhail Sukhov: 2. А вот тут не могу сказать. Как удобнее, так и надо. Тоесть Плаза за раз отсылает только одну строчку?

Насколько я понял, есть два варианта:

  1. По одной строчке в событии StreamDataInserted. В этом случае ты получаешь в качестве параметра CP2Record, который потом нужно опрашивать для каждой колонки с помощью методов GetValAsХХХ(string fieldName) / GetValAsХХХByIndex(uint fieldIndex)
  2. Подсоединять к стриму CP2DataBufferClass, который накапливает данные. Потом у него нужно вызывать метод get_TableRecords(string tableName), который возвращает CP2RecordEnumClass, что есть IEnumerable<CP2Record>И там, и сям Hello, COM world!

На мой взгляд вызовы GetValAsХХХ(string fieldName) / GetValAsХХХByIndex(uint fieldIndex) нужно реализовать внутри PlazaStream, а мне чтобы дальше писать это класс нужно понимать:

  1. PlazaStream - это служебный класс, или им смогут пользоваться клиенты (читай, сторонние разработчики, использующие PlazaTrader)?
  2. В каком виде выдавать данные классам, которые будут использовать PlazaStream? Через события по одной строчке с конвертацией CP2Record в массив object'ов? Или попробовать сделать что-то типа реализации интерфейса IDataReader с использованием CP2DataBufferClass?

И последнее насчет фильтрации данных в потоке. Есть предложение это тоже сделать внутри PlazaStream. Тогда при конвертации данных с помощью GetValAsХХХ() / GetValAsХХХByIndex() можно будет проверять только колонки, по которым идет фильтрация, и, если запись не подходит, не выдавать ее клиентскому коду.

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Дело в том, что работа с плазовским стримом построена по принципу windows message loop: в неком методе программа вызывает метод CP2ConnectionClass.ProcessMessage(out cookie, PollInterval) в бесконечном цикле, а вся информация приходит через события. Если выводить создание и управление thread'ами, это нужно делать в отдельном классе. Иначе, если придется подключаться к стримам в нескольких местах, придется дублировать много кода.

Дубляж - это плохо. И плохо, когда стрим создает потоки (кол-во потоков не должно расти с количеством стримов). Плохо делать системный класс слишком уж умным. Код должен быть один и чтобы он управлял всеми стримами.

aspirant: Я помню о настраиваемости. Пока в черновом варианте я передаю путь к файлу, чтобы сразу можно было протестировать логику подключения, опроса, получения сообщений и т.д. С метаданными такая вещь: стримы в Плазе настраиваются из файлов, даже по умолчанию (методы TableSet.InitFromIni / TableSet.InitFromIni2). Если передавать классу PlazaStream в конструкторе PlazaTable, тогда внутри PlazaStream некий класс TableConfigParser должен будет генерить файл с нужной схемой, чтобы передать ее TableSet.InitFromIni. Здесь такой момент: предлагаю в SpecialFolder.ApplicationData создать путь Stocksharp\PlazaTrader\Scheme, в котором хранить свои конфиг-схемы-файлы или их создавать в папке TEMP. Создавать их в подпапке самой Плазы не стоит: требуется админский доступ.

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

  1. Создание PlazaTrader.
  2. Опционально. Модификация таблиц с метаданными.
  3. PlazaTrader.Connect.
    1. Проверка, есть ли ini файлы, соответствующие запрашиваемым метаданным.
  1. Если нет, то они создаются на основе PlazaTable.
  2. Есть, но с другими полями, то модифицируются.
  3. Подключение к Плазе и запуск вечного цикла.

aspirant: Чтобы понимать в будущем, в каких случаях мы выкидываем PlazaException?

Только когда приходит плазовская ошибка (заявки, потоки или еще что).

aspirant: Насколько я понял, есть два варианта:

Одна особенность, все форматирование лучше помещать в PlazaFormatter. Потому как он уже используется для .NET -> Plaza при отправке транзакций. Не плохо было бы туда поместить и обратную трансформацию.

aspirant: На мой взгляд вызовы GetValAsХХХ(string fieldName) / GetValAsХХХByIndex(uint fieldIndex) нужно реализовать внутри PlazaStream, а мне чтобы дальше писать это класс нужно понимать:

  1. PlazaStream - это служебный класс, или им смогут пользоваться клиенты (читай, сторонние разработчики, использующие PlazaTrader)?
  1. В каком виде выдавать данные классам, которые будут использовать PlazaStream? Через события по одной строчке с конвертацией CP2Record в массив object'ов? Или попробовать сделать что-то типа реализации интерфейса IDataReader с использованием CP2DataBufferClass?
  1. Считаю, что нужно сделать внутренним, до тех пор, пока не попросят вынуть его наружу.🙂
  2. А нельзя просто выбрать данные в коллекцию и передавать уже ввиде родной .NET коллекции?

aspirant: И последнее насчет фильтрации данных в потоке. Есть предложение это тоже сделать внутри PlazaStream. Тогда при конвертации данных с помощью GetValAsХХХ() / GetValAsХХХByIndex() можно будет проверять только колонки, по которым идет фильтрация, и, если запись не подходит, не выдавать ее клиентскому коду.

Насчет оптимизации согласен, но опять получается ситуация со слишком умным PlazaStream.

aspirant
aspirant

Вчера разбирался со схемами таблиц потоков. Хочу выложить мои соображения:

В класс PlazaStream в конструкторе нужно будет передавать PlazaTable вместо четырех параметров: потока репликации, схемы репликации, названия таблицы и пути к конфиг-файлу.

Для PlazaTable я напишу PlazaTableSerializer на основе Ecng.Serialization.ISerializer, который будет сохранять/считывать данные в родном формате Плазы. Таким образом PlazaStream при помощи PlazaTableSerializer будет создавать временный конфиг-файл из переданного PlazaTable и с помощью этого конфига инициализировать CP2DataStreamClass.TableSet.

В связи с этим есть пара нюансов:

  • В PlazaTable нужно будет добавить свойства ReplicationStream и ReplicationScheme (Name уже есть). Они понадобятся для создания конфигов.
  • В конфиге обязательно нужно указывать тип поля таблицы в формате Плазы (i4, i8 и т.д.). Без этого CP2DataStreamClass.TableSet инициализироваться отказывается (пробовал вчера). Практически все типы данных можно сгенерить из .Net-овских (long => i8 и т.д.). Загвоздка с decimal: очень много вариантов точности и количества знаков после запятой, например, field=sell_fee,d7.3, volat_min,d20.15, synth_base_go,d26.2 и т.д. Указывать единое значение, похоже, не получится. Вчера пробовал менять значение одного поля в таблице rts_index: CP2DataStreamClass.TableSet инициализируется, но при получении данных валятся ошибки, а данных нет. Кстати, по аналогии похожая проблема будет и со строковыми полями: там тоже разные значения длины поля. Пока видится только один выход: для каждой колонки типа decimal (и скорее всего string) указывать свое значение.
Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Вчера разбирался со схемами таблиц потоков. Хочу выложить мои соображения:

Да, с точностью получился косяк. Надо исправлять.

skuvv, вы все еще на метаданных? Тогда не пропустите момент, который указал aspirant.

skuvv
skuvv

Mikhail Sukhov:

aspirant: Вчера разбирался со схемами таблиц потоков. Хочу выложить мои соображения:

Да, с точностью получился косяк. Надо исправлять.

skuvv, вы все еще на метаданных? Тогда не пропустите момент, который указал aspirant. Добавил для всех сделанных мета-таблиц соответствующий PlazaType для decemical и string

aspirant
aspirant

skuvv:

Mikhail Sukhov:

aspirant: Вчера разбирался со схемами таблиц потоков. Хочу выложить мои соображения:

Да, с точностью получился косяк. Надо исправлять.

skuvv, вы все еще на метаданных? Тогда не пропустите момент, который указал aspirant. Добавил для всех сделанных мета-таблиц соответствующий PlazaType для decemical и string

Круто😀. Я только собрался писать об этом изменении. Даже сам еще свои таблицы не исправил.

aspirant
aspirant

Mikhail Sukhov: Вообщем, логика мне виделась такая (если такое невозможно, то поправьте):

  1. Создание PlazaTrader.
  1. Опционально. Модификация таблиц с метаданными.
  1. PlazaTrader.Connect.
  1. Проверка, есть ли ini файлы, соответствующие запрашиваемым метаданным.
  1. Если нет, то они создаются на основе PlazaTable.
  1. Есть, но с другими полями, то модифицируются.
  1. Подключение к Плазе и запуск вечного цикла.

Этап 2 (опционально): создание таблиц PlazaTable кодом клиента (либо нужный набор колонок, либо все) + наш код парсит все ini файлов в заданной директории для создании на их основе таблиц PlazaTable (через метод PlazaTableSerializer.Deserialize(string fileName) + отслеживаем изменения/добавления файлов в этой директории. Здесь, кстати, уточнение: в предыдущем посте я был не прав. вот, что написано в документации (P2ClientGate.doc стр. 18): "Клиент также может не указывать никакой схемы при открытии потока, в этом случае сервер будет отдавать все данные, которые публикуются в потоке (далее этот режим будет называться «получение данных по схеме сервера»)." Т.е., если в потоке несколько таблиц, будут выдаваться данные по всем таблицам.

Для управления стримами (PlazaStream) я создам PlazaStreamManager, который будет внутри себя создавать стримы, подписываться на их события и управлять их подключением / отключением (т.е. внутри него будет бесконечный цикл). В PlazaStreamManager код пользователя передает коллекцию таблиц (по одной или сразу IEnumerable<PlazaTable>) из предыдущего пункта.

Этап 3 (PlazaTrader.Connect): если в PlazaStreamManager есть стримы, запускаем его. Все стримы будут вертеться в одном потоке (thread). Как дополнительную опцию (третьестепенная / четвертостепенная задача), добавить возможность создания приоритетных стримов в отдельном потоке (thread). Не знаю, нужно ли это?

Mikhail Sukhov: 2. А нельзя просто выбрать данные в коллекцию и передавать уже ввиде родной .NET коллекции?

Насколько я понял, принцип работы со стримами в Плазе следующий: создаем и инициализируем CP2DataStream, затем CP2TableSet (у каждого стрима только один CP2TableSet). Далее подписываемся на события, открываем стрим. После "рукопожатия" (StreamLifeNumChanged) приходит событие StreamDataBegin - данные пошли. Они идут через StreamDataInserted по одной строке. Когда некий объем данных заканчивается, приходит StreamDataEnd. Мне интересно, как часто Plaza выдает событие StreamDataEnd. Хочу в ближайшие пару дней запустить какую-нибудь тестовую программу на пару часов и посмотреть. Если прерывания случаются часто, то действительно можно копить строки (записи) в PlazaStreamManager и выдавать клиенту, например, DataTable или DataRow[] и т.д. Вместе с этим, мне кажется, нужно дать возможность клиенту тоже получать наш вариант StreamDataInserted, тем более что кроме него там еще есть StreamDataUpdated и StreamDataDeleted. В этих событиях можно через PlazaFormatter конвертировать данные в .Net'овские, помещать их в List<object> и выдавать последний клиенту, как object[].

Последнее, насчет фильтрации. Можно будет предусмотреть некий интерфейс IPlazaFilter с методом bool Validate(CP2Record rec, + что-нибудь еще). В методе PlazaStreamManager, который создает новый стрим, можно передавать IEnumerable<IPlazaFilter> или null, который будет привязываться к конкретному стриму. Тогда PlazaStreamManager будет перебирать для каждого стрима его IEnumerable<IPlazaFilter> в событиях StreamDataInserted / StreamDataUpdated / StreamDataDeleted, и если какой-то IPlazaFilter возвращает false, не выдавать событие с данными клиенту.

aspirant
aspirant

Только что залил изменения. Хотел поделиться промежуточными итогами:

  1. В класс PlazaColumns добавил три поля: ReplicationStream ReplicationStream { get; } string Table { get; } ReplicationScheme ReplicationScheme { get; } Пока сделал их все virtual, чтобы код компилировался, но вообще первые два поля должны быть abstract, чтобы все классы-наследники их переписали для себя. Правильные объявления я пока закомментил. Эти три поля часть метаданных (у каждого наследника PlazaColumns может быть только один набор значений ReplicationStream - ReplicationScheme - Table) и нужны для инициализации PlazaTable, чтобы потом можно его было передать в PlazaStream или просто сохранить на диск через PlazaTableSerializer.
  2. Написал первый вариант PlazaStreamManager: пока только создает / удаляет объекты PlazaStream и перехватывает их события. Весь "ум" из класса PlazaStream убрал. Теперь это по сути обертка для CP2DataStreamClass. Его нужно будет протестировать со всеми наследниками наследника PlazaColumns на предмет правильности описания метаданных. Я его тестировал с PlazaRtsIndexColumns и еще парой классов. Кстати, при запуске метода Start() PlazaStreamManager начинает заметно подтормаживать GUI-поток .Net'овскго тестового проекта.
  3. Получение данных пока только получалось тестировать после работы: приходит сразу некая порция строк плазовского потока репликации и все. Днем один раз получилось запустить PlazaStream + PlazaRtsIndexColumns: данные сыпались непрерывно где-то минут пять точно, потом мне нужно было идти. Хотелось получить ваши комментарии, рекомендации и т.д., после чего продолжу работу на PlazaStreamManager. Там еще полно дел. Кроме того еще нужно дописать PlazaTableSerializer.
Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Хотелось получить ваши комментарии, рекомендации и т.д., после чего продолжу работу на PlazaStreamManager. Там еще полно дел. Кроме того еще нужно дописать PlazaTableSerializer.

  1. Плохой код.
try
{
	_dataStream.Close();
}
catch (System.Runtime.InteropServices.COMException e)	
{
	// Исправить обработку исключения
	System.Diagnostics.Trace.WriteLine("Exception {0:X}: {1}".Put(e.ErrorCode, e.Message));
}

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

_dataStream.TableSet.set_rev(plazaTable.Name, _currentRevision++);

_dataStream.TableSet.rev[plazaTable.Name] = _currentRevision++;


4. ReplicationStream и ReplicationScheme явно как то не так используется. Это же уровень метаданных, а именно таблица PlazaTable (таблица олицетворяет стрим данных, PlazaStream по этом олицетворению производит подключение). А получилось так, что эти два перечисления размазались по всем практически классам в Плазе. Нехорошо. Конструктора PlazaStream(PlazaTable plazaTable, TRequestType requestType) более чем достаточно. Зачем нужен еще один я не понял.

5. public void AddStream(string key, PlazaColumns schema, IEnumerable<PlazaColumn> columns) Выглядит как то не так. Зачем столько аргументов? Можно же ведь передать один PlazaTable и уже по нему создать стрим, и уже оперировать им в дальнейшем.
Mikhail Sukhov
Mikhail Sukhov 作者

Увидел такую подписку:

plazaStream.DataBegin += (p) => OnPlazaStreamDataBegin(key, p);

Она плоха тем, что при таком подписывании нельзя будет отписаться от события и объект будет продолжать висеть в памяти (не так страшно при существующих гигабайтах, но все же). Лямбда выражения создают "на лету" метод обработчик, от которого потом не отписаться. Чтобы производить правильное освобождение ресурсов, нужно от них отказать вообще и писать по старинке:

plazaStream.DataBegin += OnPlazaStreamDataBegin;

Я думаю, лучше сразу проговорить для чего вообще нужны лямбда. Они появились тогда, когда в .NET начал входить функциональный стиль. Для этого и только для этого их и придумали (явный пример - это LINQ). Я в своих примерах использовал лямбду не потому что так правильно. А потому что мне было лень писать правильно, и быстрее + короче написать как это сделали Вы. Но внутри S# я так конечно же не делаю, к чему призываю делать и в ПлазаТрейдере.😂

aspirant
aspirant

Mikhail Sukhov:

  1. Плохой код.

try { _dataStream.Close(); } catch (System.Runtime.InteropServices.COMException e) { // Исправить обработку исключения System.Diagnostics.Trace.WriteLine("Exception {0:X}: {1}".Put(e.ErrorCode, e.Message)); }

> 
> Исключения нужно перехватывать как можно выше. Кто будет вызывать Close, тот пусть и этим занимается. А так ошибка просто проглотиться.
Я пока сделал такую заплату, чтобы можно было тестировать код (поэтому и оставил для себя комментарий).  Вообще мне кажется, COMException нужно везде перехватывать и выдавать свои исключения, чтобы внешнему коду не видеть внутреннюю кухню.  Если внешний код имеет дело с PlazaStream, то он и должен оперировать понятиями (читай методами, статусами и т.д.) PlazaStream.

> **[Mikhail Sukhov](@message(5657)):**
> 3. ```
_dataStream.TableSet.set_rev(plazaTable.Name, _currentRevision++);
``` Это меняется на более элегантное, как подсказал R#:
> ```
_dataStream.TableSet.rev[plazaTable.Name] = _currentRevision++;

исправлю.

Mikhail Sukhov: 4. ReplicationStream и ReplicationScheme явно как то не так используется. Это же уровень метаданных, а именно таблица PlazaTable (таблица олицетворяет стрим данных, PlazaStream по этом олицетворению производит подключение). А получилось так, что эти два перечисления размазались по всем практически классам в Плазе. Нехорошо. Конструктора PlazaStream(PlazaTable plazaTable, TRequestType requestType) более чем достаточно. Зачем нужен еще один я не понял.

  1. public void AddStream(string key, PlazaColumns schema, IEnumerable<PlazaColumn> columns) Выглядит как то не так. Зачем столько аргументов? Можно же ведь передать один PlazaTable и уже по нему создать стрим, и уже оперировать им в дальнейшем.

Иерархия данных в потоках репликации: ReplicationStream -> ReplicationScheme -> Table -> Field, т,е. у каждой плазовской таблицы (в PlazaTrader это PlazaColumns) есть только одно значение ReplicationStream и ReplicationScheme, поэтому я и счел нужным добавить эти поля в PlazaColumns, чтобы при создании PlazaStream значения ReplicationStream и ReplicationScheme можно было вычислить из PlazaColumns.

Есть два варианта получения данных репликации. Во-первых, можно передать Плазе полный или урезанный набор полей какой-то таблицы (создавать "свои" таблицы с полями из нескольких таблиц нельзя). Второй вариант запроса данных - без указания схемы (см. P2ClientGate.doc стр. 18: "Клиент также может не указывать никакой схемы при открытии потока, в этом случае сервер будет отдавать все данные, которые публикуются в потоке (далее этот режим будет называться «получение данных по схеме сервера»)". Поскольку такой вариант подключения существует, я предусмотрел второй конструктор PlazaStream, в котором только передается ReplicationStream. При таком подключении при получении данных через события нужно будет проверять название таблицы, именно поэтому у CP2DataStreamClass второй параметр событий StreamDataInserted, StreamDataUpdated и StreamDataDeleted - это string tableName.

AddStream(string key, PlazaColumns schema, IEnumerable<PlazaColumn> columns) я исправлю на AddStream(string key, PlazaTable plazaTable)

aspirant
aspirant

Mikhail Sukhov: Увидел такую подписку:

plazaStream.DataBegin += (p) => OnPlazaStreamDataBegin(key, p);

> 
> Она плоха тем, что при таком подписывании нельзя будет отписаться от события и объект будет продолжать висеть в памяти (не так страшно при существующих гигабайтах, но все же). Лямбда выражения создают "на лету" метод обработчик, от которого потом не отписаться. Чтобы производить правильное освобождение ресурсов, нужно от них отказать вообще и писать по старинке:
> 
> ```
plazaStream.DataBegin += OnPlazaStreamDataBegin;

Я думаю, лучше сразу проговорить для чего вообще нужны лямбда. Они появились тогда, когда в .NET начал входить функциональный стиль. Для этого и только для этого их и придумали (явный пример - это LINQ). Я в своих примерах использовал лямбду не потому что так правильно. А потому что мне было лень писать правильно, и быстрее + короче написать как это сделали Вы. Но внутри S# я так конечно же не делаю, к чему призываю делать и в ПлазаТрейдере.😂

Это пока временный вариант, потому что я пока еще не определился с окончательным набором параметров событий. По ходу написания добавил string key, а поскольку хотелось залить первый черновой вариант, оставил как есть.

Свои делегаты я убрал вообще. Вы правы, они лишние. Все вызовы событий передал через SafeInvoke.

aspirant
aspirant

Кстати, только что заметил, что мой PlazaRtsIndexColumns дублируется с PlazaIndexColumns. Сейчас удалю свое детище😢

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Кстати, только что заметил, что мой PlazaRtsIndexColumns дублируется с PlazaIndexColumns. Сейчас удалю свое детище😢

А csproj до сих пор на него ссылается. Коммитить нужно все атомарно.

skuvv
skuvv

Добавил класс с кодами ошибок плазы - PlazaErrors парсит код ошибки и возвращает текст ps ктото начал заниматься ошибками - пользуйтесь

Mikhail Sukhov
Mikhail Sukhov 作者

skuvv: Добавил класс с кодами ошибок плазы - PlazaErrors парсит код ошибки и возвращает текст ps ктото начал заниматься ошибками - пользуйтесь

Я этим занимаюсь😊. Но пока нет времени доделать, так что большое спасибо, что помогли. Но раз уж сделали, вот и Вам code review:

  1. public class Errors. Его не нужно сделать статическим? Нужен ли он для public?
  2. Error_Code. Лучше параметры называть в C# стиле.
  3. Dictionary<int, List> List. Конечно же, это никакой не Лист. Но и Дикшинари тоже будет неправильно назвать. У этого поля должно быть осмысленное имя.
  4. List.ElementAt(i) - лучше использовать List. Так быстрее, особенно если это Dictionary.
  5. BitConverter.GetBytes(Error_Code) Можно написать проще - Error_Code.To<byte[]>().
skuvv
skuvv

Mikhail Sukhov:

skuvv: Добавил класс с кодами ошибок плазы - PlazaErrors парсит код ошибки и возвращает текст ps ктото начал заниматься ошибками - пользуйтесь

Я этим занимаюсь😊. Но пока нет времени доделать, так что большое спасибо, что помогли. Но раз уж сделали, вот и Вам code review:

  1. public class Errors. Его не нужно сделать статическим? Нужен ли он для public?
  2. Error_Code. Лучше параметры называть в C# стиле.
  3. Dictionary<int, List<string>> List. Конечно же, это никакой не Лист. Но и Дикшинари тоже будет неправильно назвать. У этого поля должно быть осмысленное имя.
  4. List.ElementAt(i) - лучше использовать List. Так быстрее, особенно если это Dictionary.
  5. BitConverter.GetBytes(Error_Code) Можно написать проще - Error_Code.To<byte[]>(). Просто готовый был класс 🙂 насчет 4. в Dictionary таким способом (List) доступ к ключу не получить вроде
aspirant
aspirant

Михаил,

Вопрос и комментарий.

Вопрос: еще раз хотел вернуться, потому что не совсем для себя уяснил. Как вам видится (изначально виделась) работа клиентского кода с PlazaTrader в применении к плазовским стримам (потоками репликации). При запуске PlazaTrader из клиентского кода, PlazaTrader подключается

  1. к стримам, чьи схемы расписаны в ini-файлах специальной папке?
  2. и/или клиентский код создает схемы программным путем на лету? Во втором случае это будет происходить через создание экземпляра PlazaTable и передачу его в PlazaTrader или мы оставим PlazaStreamManager public? Сейчас он сделан public только в целях тестирования (чтобы можно было вызывать напрямую из тестового проекта).

Комментарий: наверное, перечисление PlazaTableTypes будет лишним. Как я понимаю, оно сделано для проверки совместимости PlazaTable и PlazaColumn (см. внизу)

Mikhail Sukhov: Для чего нужен тип таблицы. Для того, чтобы предотвращать в пользовательском коде такие вещи:

plazaTrader.<mark>SecuritiesTable</mark>.Columns.Add(PlazaColumns.<mark>OrdersColumns</mark>.Direction)

и выбрасывать исключения при запуске программы.

Но тогда подразумевается, что в PlazaTable можно передать колонки из разных наследников PlazaColumns, но с одинаковым PlazaTableTypes. А это не пройдет. Еще раз повторюсь: "синтетические" схемы из колонок разных плазовских схем (PlazaColumns) создавать нельзя. Корректным при наполненни PlazaTable набором PlazaColumn'ов будет проверять каждую PlazaColumn на одинаковость с PlazaTable по ReplicationStream, ReplicationScheme и Table(Name).

Что вы на этот счет думаете?

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Вопрос: еще раз хотел вернуться, потому что не совсем для себя уяснил. Как вам видится (изначально виделась) работа клиентского кода с PlazaTrader в применении к плазовским стримам (потоками репликации). При запуске PlazaTrader из клиентского кода, PlazaTrader подключается

  1. к стримам, чьи схемы расписаны в ini-файлах специальной папке?
  1. и/или клиентский код создает схемы программным путем на лету?

Во втором случае это будет происходить через создание экземпляра PlazaTable и передачу его в PlazaTrader или мы оставим PlazaStreamManager public? Сейчас он сделан public только в целях тестирования (чтобы можно было вызывать напрямую из тестового проекта).

И то и другое. При старте настройки PlazaTable берутся из уже созданных ini файлов. Если их нет, то создается минимально допустимый набор метаданных (как это было сделано с Quik таблицами) и по свеже созданным таблицам создаются соответствующие ini файлы. Если клиентский код модифицирует PlazaTable, его действия так же применяются и к ini файлам.

Но мне не понятно эта иерархия. Стрим - репликация - таблица. Это реально нужная вещь, без которой Плаза не взлетит? Пока выглядит как избыточность.

aspirant: Но тогда подразумевается, что в PlazaTable можно передать колонки из разных наследников PlazaColumns, но с одинаковым PlazaTableTypes.

Я думаю, неправильно было изначально делать разные PlazaColumns с одинаковыми PlazaTableTypes. Я же не знал, что для одной сущности в Плазе триллион таблиц соответствует. Потому и написал изначально, Position Portfolio и т.д. Так что да, или его переделывать, или его выкидывать.

aspirant
aspirant

Mikhail Sukhov: Но мне не понятно эта иерархия. Стрим - репликация - таблица. Это реально нужная вещь, без которой Плаза не взлетит? Пока выглядит как избыточность.

Это не мои изобретения. Выдача данных клиентов Плазой строится следующим образом: есть несколько потоков репликации (FORTS_OPTINFO_REPL и т.д.). У каждого потока есть dbscheme. Вроде у каждого потока она одна, но пока не успел проверить точно. Зачем она нужна, если она одна, - ума не приложу. И наконец в каждом потоке есть от одной до несколько таблиц. Это их мы описывали наследниками PlazaColumns.

Чтобы подписаться на потоки и получать данные, я могу просто указать имя потока (второй конструктор PlazaStream). В этом случае будут приходить ВСЕ данные потока, т.е. если в нем несколько таблиц, например в FORTS_FUTINFO_REPL их 17 штук, их данные будут приходить через событие CP2DataStreamClass.StreamDataInserted вперемешку. Второй вариант - это, когда я отправляю на сервер содержимое ini-файла таблицы с обязательным указанием потока репликации, dbscheme, именем таблицы и списка нужных колонок запрашиваемой таблицы. В этом случае, понятно, приходят только запрошенные колонки запрошенной таблицы.

Из-за того, что процесс подписки на потоки построен именно таким образом, я считаю необходимым добавить в PlazaColumns

public abstract ReplicationStream ReplicationStream { get; } public virtual ReplicationScheme ReplicationScheme { get { return ReplicationScheme.CustReplScheme; } } public abstract string Table { get; }

У каждой таблицы потока репликации, т.е. наследника PlazaColumns, своя уникальная неизменная комбинация ReplicationStream + ReplicationScheme + Table. Она мне нужна для создания ini-файла.

Букв много. Надеюсь, что не запутал окончательно.

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: public abstract ReplicationStream ReplicationStream { get; } public virtual ReplicationScheme ReplicationScheme { get { return ReplicationScheme.CustReplScheme; } } public abstract string Table { get; }

У каждой таблицы потока репликации, т.е. наследника PlazaColumns, своя уникальная неизменная комбинация ReplicationStream + ReplicationScheme + Table. Она мне нужна для создания ini-файла.

Букв много. Надеюсь, что не запутал окончательно.

Стало чуть понятнее. А где теперь делать проверку, что в таблицу попала правильная колонка?

aspirant
aspirant

Mikhail Sukhov: А где теперь делать проверку, что в таблицу попала правильная колонка?

Предлагаю у PlazaColumn сделать readonly поле Owner типа PlazaColumns, инициализировать его в конструкторе PlazaColumn. В конструкторе PlazaTable перебирать IEnumerable<PlazaColumn> columns и сравнивать у каждого элемента Owner.ReplicationStream, Owner.ReplicationScheme и Owner.Table c replicationStream, replicationScheme, переданными в конструктор PlazaTable.

Как вариант, можно убрать из параметров конструктора PlazaTable параметры ReplicationStream replicationStream, ReplicationScheme replicationScheme, string name вообще и считать, что правильные параметры ReplicationStream, ReplicationScheme, name указаны в первом элементе IEnumerable<PlazaColumn> columns. Соответственно, проверку на правильность остальных элементов IEnumerable<PlazaColumn> columns проводить с значениями полей первого элемента.

Не знаю, какой вариант правильнее?

aspirant
aspirant

Михаил,

Хочу предложить серьезные изменения в структуре метаданных. Вместе с обоснованием хочу для наглядности выложить пример на WPF. Как это лучше сделать?

Еще, если я залью на сервер изменения, я так понимаю, их же можно будет потом назад откатить?

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Хочу предложить серьезные изменения в структуре метаданных. Вместе с обоснованием хочу для наглядности выложить пример на WPF. Как это лучше сделать?

Написать сюда или картинку вставить.

aspirant: Еще, если я залью на сервер изменения, я так понимаю, их же можно будет потом назад откатить?

Можно, но муторно как и любой откат.

aspirant
aspirant

Что я предлагаю:

  1. Сейчас все метаданные описаны классами-наследниками PlazaColumns, что не совсем соответствует иерархии метаданных самой Плазы. Я предлагаю создать абстрактный класс PlazaReplicationStream (название можно как-нибудь сократить, например PlazaReplStream), наследники которого будут соответствовать потокам репликации, например, FortsOptTradeReplicationStream - FORTS_OPTTRADE_REPL, RtsIndexReplicationStream - RTS_INDEX_REPL и т.д. По аналогии со структурой PlazaColumns в самом PlazaReplicationStream будут открытые переменные readonly для каждого потока репликации, например
public static readonly FortsOptTradeReplicationStream FortsOptTradeRepl = new FortsOptTradeReplicationStream();
public static readonly RtsIndexReplicationStream RtsIndexRepl = new RtsIndexReplicationStream();
  1. PlazaColumns предалагю переименовать в PlazaTable, потому что это все-таки таблицы, а не наборы колоннок, которые можно между собой смешивать (но это лишь моя интерпретация названия PlazaColumns). В каждом классе потоке репликации будут свои наследники PlazaTable, что будет соответствовать содержанию самих потоков репликации в Плазе. Так, что если в FORTS_OPTTRADE_REPL есть таблицы orders_log, deal, heartbeat, в классе FortsOptTradeReplicationStream будут такие поля:``` public readonly PlazaOptionOrdersLogColumns OrdersLog = new PlazaOptionOrdersLogColumns(); public readonly PlazaOptionDealColumns Deal = new PlazaOptionDealColumns(); public readonly PlazaHeartBeatColumns HeartBeat;
Теперь, если нужно подписаться на весь поток FORTS_OPTTRADE_REPL и получать данные по всем таблицам, это можно сделать так:```
_plazaStreamManager.AddStream("FortsOptTradeRepl_OrdersLog", PlazaReplicationStream.FortsOptTradeRepl);

А, если нужно подписаться только на одну таблицу, например orders_log, это можно сделать так``` _plazaStreamManager.AddStream("FortsOptTradeRepl_OrdersLog", PlazaReplicationStream.FortsOptTradeRepl.OrdersLog);

Если нужно передать только отдельные колонки, то это делатется так:```
_plazaStreamManager.AddStream("FortsOptTradeRepl_OrdersLog", PlazaReplicationStream.FortsOptTradeRepl.OrdersLog, columns1);

где columns1 это``` var columns1 = new PlazaColumn[] { PlazaReplicationStream.FortsOptTradeRepl.OrdersLog.ReplId, PlazaReplicationStream.FortsOptTradeRepl.OrdersLog.ReplRev, PlazaReplicationStream.FortsOptTradeRepl.OrdersLog.ReplAct, PlazaReplicationStream.FortsOptTradeRepl.OrdersLog.Price, PlazaReplicationStream.FortsOptTradeRepl.OrdersLog.Hedge };

Кстати, уже сейчас по ходу написания родилась мысль, что в последнем примере вместо IEnumerable<PlazaColumn> можно будет передавать новый класс, называющийся, например, PlazaColumnListBuilder, который, во-первых, будет за клиента автоматом добавлять первыми тремя колонками служебные колонки ReplId, ReplRev, ReplAct (это обязательно, иначе не подключишься), а, во-вторых, будет проверять, чтобы все колонки были одной таблицы.
3. Нынешний PlazaTable предлагаю переименовать в PlazaTableConfiguration.  Он будет уметь сохранять заданную конфигурацию таблицы (читай нужный набор колоннок) в ini-файле в формате Плазы, а также создаваться из таких файлов для последующего подключения к потокам репликации.
4. PlazaTableTypes нужно, наверное, либо прицепить к наследникам PlazaReplicationStream, либо убрать вообще.
Если мои предложения принимаются, я могу начать вносить изменения.  Также беру на себя рефакторинг всех наследников PlazaColumns.  Постараюсь сделать на этой неделе до конца выходных.
Mikhail Sukhov
Mikhail Sukhov 作者

Не могу сейчас развернуто вникнуть в суть проблемы.

  1. Я думаю это неправильно. Иерархия Плазы как раз сама по себе избыточная (я так понимаю тогда задача стояла сделать что то по быстрому, а не что-то стоящее). Идти за их иерархией - просто повторять их ошибки.

  2. PlazaTable - это то что конфигурируется пользователем. PlazaColumns - базовый класс, содержащийся основные поля. Даже не представляю, как одно может заменить другое... Плюс дальше по описанию вижу, что пользователю предлагается вынести управление потоками. Я однозначно понял за последнее время - этого делать не нужно. Лучше придерживать принципа инкапсуляции. Разрушить его можно в любое время (что опять же не очень хорошо).

  3. Скажу так, все эти PlazaColumnListBuilder PlazaTableConfiguration говорит о том, что уже начался овер дизайн.😀 Надо как-то проще. Потому что сейчас уже не совсем понятно. Что будет потом? А что будет с теми пользователями, которые захотят переползти с голой Плазы? Одно сложное на другое сложно не меняют.

aspirant
aspirant

Похоже я нагородил кучу лишнего. Короче все это нужно отринуть.

МИХАИЛ: вопрос, который меня занимает сейчас больше всего: какой класс будет содержать все себе данные, которые будут приходить в потоках Плазы? Если PlazaStream мы не будем показывать клиентскому коду, тогда, может быть, это будет PlazaTable? А если не он, то что это будет за класс, и как он будет связан с PlazaTable?

По задачам, связанными с подпиской на потоки репликации Плазы я вижу, что нужно сделать следующее:

  1. Создать буфер накопления входящих данных стрима.
  2. Расписать методы Deserialize у PlazaTableSerializer, чтобы можно было инициализировать PlazaStream из ini файлов. Прикрутить инфраструктуру инициализации из файлов к PlazaStreamManager/PlazaTrader.
  3. Фильтрация входящих данных.
  4. Создание пула CP2ConnectionClass'ов для оптимизации и возможности параллельного получения данных по потокам. Сейчас, насколько я понял из тестирования, если данные из нескольких стримов идут по одному CP2ConnectionClass, это происходит порционно, т.е. идет партия данных одного стрима, за ней партия данных другого стрима и т.д.

Я сейчас занимаюсь первым пунктом, а остальные пункты можно разобрать на задачи. Самым сложным и интересным вижу пункт 3.

Mikhail Sukhov
Mikhail Sukhov 作者

aspirant: Похоже я нагородил кучу лишнего. Короче все это нужно отринуть.

МИХАИЛ: вопрос, который меня занимает сейчас больше всего: какой класс будет содержать все себе данные, которые будут приходить в потоках Плазы? Если PlazaStream мы не будем показывать клиентскому коду, тогда, может быть, это будет PlazaTable? А если не он, то что это будет за класс, и как он будет связан с PlazaTable?

Я думал что это будет или object[] или object[][]... Это сильно критично через что данные будут передаваться?

aspirant: По задачам, связанными с подпиской на потоки репликации Плазы я вижу, что нужно сделать следующее:

  1. Создать буфер накопления входящих данных стрима.
  1. Расписать методы Deserialize у PlazaTableSerializer, чтобы можно было инициализировать PlazaStream из ini файлов. Прикрутить инфраструктуру инициализации из файлов к PlazaStreamManager/PlazaTrader.
  1. Фильтрация входящих данных.
  1. Создание пула CP2ConnectionClass'ов для оптимизации и возможности параллельного получения данных по потокам. Сейчас, насколько я понял из тестирования, если данные из нескольких стримов идут по одному CP2ConnectionClass, это происходит порционно, т.е. идет партия данных одного стрима, за ней партия данных другого стрима и т.д.

Я сейчас занимаюсь первым пунктом, а остальные пункты можно разобрать на задачи. Самым сложным и интересным вижу пункт 3.

Ок, тогда по первому задавать вопросы не буду.

  1. Тоесть, PlazaTableSerializer восстанавливает из конфига PlazaTable и он уже передается в PlazaStream?
  2. Распиши, как ты это видишь и где это нужно сделать.
  3. Как насчет для каждого стрима создавать по отдельному CP2ConnectionClass? Можно пока задачу подвинуть как оптимизациюнную (а следовательно не приоритетную😀)
aspirant
aspirant

Mikhail Sukhov: Я думал что это будет или object[] или object[][]... Это сильно критично через что данные будут передаваться? Просто, чтобы понимать куда передавать данные дальше. Сейчас они приходят по одной записи/строчке в методе void OnStreamDataInserted(CP2DataStream stream, string tableName, CP2Record record) класса PlazaStream.

Mikhail Sukhov: 2. Тоесть, PlazaTableSerializer восстанавливает из конфига PlazaTable и он уже передается в PlazaStream?

Да, причем, на мой взгляд, клиентскому коду лучше не давать доступа к PlazaTableSerializer. Пускай он это лучше делает через методы Load/Save класса PlazaTable. Формат ini-файлов это внутренность Плазы, и ее лучше, хотя бы на первых порах, инкапсулировать от клиентского кода.

Mikhail Sukhov: 3. Распиши, как ты это видишь и где это нужно сделать.

Создать интерфейс IPlazaStreamFilter, у которого будет метод bool Validate(object value) и свойство string ColumnName. В конструктор PlazaTable передавать IEnumerable<IPlazaStreamFilter>, который, в свою очередь будет эту энумерацию передавать в конструктор PlazaStream. Внутри себя PlazaStream в методе void OnStreamDataInserted(CP2DataStream stream, string tableName, CP2Record record) будет перебирать эту энумерацию, вызывать для каждого элемента метод Validate(object Value) и передавать данные, приходящие в потоке репликации, дальше (в клиентский код) только, если все вызовы Validate(object Value) возвращают true. Но это только верхушка айсберга, потому что, оперируя понятиями SQL, мы только реализуем AND связки, например, price>100 AND code_vcb=xyz. А нужно еще предосмотреть, как минимум связку OR, например, price>100 OR code_vcb=xyz. Дальше я пока над этим не думал.

Mikhail Sukhov: 4. Как насчет для каждого стрима создавать по отдельному CP2ConnectionClass? Можно пока задачу подвинуть как оптимизациюнную (а следовательно не приоритетную😀) Да, это точно на потом. Сначала нужно с первыми тремя задачами разобраться.

guk
guk

Взял > 4. Составные инструменты. из > Третьестепенная группа: