Static, dynamic, keys, DeepSeek, EventHub
Мы имеем два типа ключей: статические и динамические.
Статический ключ: это строка, которая не меняется от сообщения к сообщению.
Например, "quote:generated" для всех событий котировок, независимо от символа.
Динамический ключ: это строка, которая может меняться в зависимости от данных сообщения.
Например, "quote:generated:AAPL" для котировок конкретного символа AAPL.
Разница в написании:
Статический ключ: всегда одна и та же строка.
Динамический ключ: строится с использованием данных сообщения (например, символа).
Разница в действии:
Со статическим ключом все события одного типа имеют одинаковый ключ, и подписчик получает все события этого типа.
С динамическим ключом подписчик может подписаться на конкретный ключ (например, только на котировки AAPL) и получать только соответствующие события.
В вашем коде вы использовали атрибут [MessageKey] со статическим ключом, а в MessageBase передавали динамический ключ (с символом).
Это приводит к несоответствию: атрибут говорит одно, а фактический ключ (из MessageBase) - другое.
В системе маршрутизации (CompositeKeyRouter) используется ключ из MessageBase (тот, который передается в конструктор).
Атрибут [MessageKey] используется для регистрации статического ключа, но если MessageBase создает другой ключ, то сообщение будет маршрутизировано по ключу из MessageBase.
Пример:
[MessageKey("trade:executed")]
public record TradeExecutedEvent(...) : MessageBase($"trade:{Symbol}");
При публикации TradeExecutedEvent для символа "AAA" будет создано сообщение с ключом "trade:AAA".
Но атрибут говорит, что статический ключ "trade:executed". Это разные ключи.
Если вы подпишетесь на "trade:executed", то не получите это сообщение, потому что оно было опубликовано с ключом "trade:AAA".
Как исправить?
Вариант 1: Использовать статические ключи для всех событий.
Измените MessageBase на статический ключ, например: MessageBase("trade:executed")
Вариант 2: Использовать динамические ключи и подписываться на них с помощью масок (если ваша система поддерживает).
Но в текущей реализации BusLibrary02, судя по коду, нет поддержки масок в ключах.
Вариант 3: Оставить как есть, но подписываться на динамические ключи, предварительно зная символы.
Например, подписаться на "trade:AAA", "trade:BBB" и т.д. Но это неудобно, если символов много.
Вариант 4: Использовать и статический, и динамический ключ одновременно. Но для этого нужно изменить систему маршрутизации.
Вариант 5: Использовать два отдельных механизма:
- статические ключи для глобальных подписок (через атрибуты)
- динамические ключи для точных подписок (через IDynamicSubscriptionManager)
Однако, в текущей системе (CompositeKeyRouter) используется только один ключ для маршрутизации.
Вам нужно решить, какой подход вам нужен.
Рекомендую использовать статические ключи для всех событий, если только вам действительно не нужна фильтрация по символам на уровне маршрутизации.
Если нужна фильтрация, то можно использовать статический ключ и фильтровать в обработчике.
Пример статического ключа:
[MessageKey("trade:executed")]
public record TradeExecutedEvent(
string Symbol,
...
) : MessageBase("trade:executed");Тогда в обработчике вы можете проверить Symbol и решить, обрабатывать ли это сообщение.
Пример динамического ключа (если вы хотите, чтобы разные символы шли разным подписчикам):
[MessageKey("trade:executed:{Symbol}")]
public record TradeExecutedEvent(
string Symbol,
...
) : MessageBase($"trade:executed:{Symbol}");Но тогда подписчик должен знать символы, на которые он подписывается.
В вашем случае, если вы хотите, чтобы DatabaseBackgroundService получал все сделки, независимо от символа, то используйте статический ключ.
Если же вы хотите, чтобы были разные подписчики для разных символов, то используйте динамический ключ.
Учитывая, что вы хотите сохранять все в базу данных, скорее всего, вам нужны все сделки, поэтому статический ключ.
Однако, если вы планируете в будущем иметь отдельные базы данных для разных символов, то динамический ключ.
Но в текущей реализации InMemoryDatabase хранит все в одном месте, поэтому статический ключ предпочтительнее.
Давайте изменим ваши события на статические ключи.
Пример изменения для TradeExecutedEvent:
Было:
[MessageKey("trade:executed")]
public record TradeExecutedEvent(...) : MessageBase($"trade:{Symbol}");
Стало:
[MessageKey("trade:executed")]
public record TradeExecutedEvent(...) : MessageBase("trade:executed");Тогда в DatabaseBackgroundService вы можете подписаться на "trade:executed" и получать все сделки.
Аналогично для других событий.
Но обратите внимание: в вашем коде уже есть другие сервисы, которые подписываются на события.
Например, TradingMonitorService подписывается на TradeExecutedEvent, QuoteGeneratedEvent и PositionChangedEvent.
Если вы измените ключи, то нужно убедиться, что все подписки используют правильные ключи.
В TradingMonitorService вы используете IDynamicSubscriptionManager.Subscribe<TradeExecutedEvent> (без указания ключа).
Этот метод использует статический ключ, зарегистрированный для типа TradeExecutedEvent (через атрибут или через RegisterStaticKey).
Поэтому, если вы измените атрибут и MessageBase на статический ключ, то подписка в TradingMonitorService будет работать.
Итог:
Рекомендую для всех событий использовать статические ключи. Примеры:
QuoteGeneratedEvent:
[MessageKey("quote:generated")]
public record QuoteGeneratedEvent(...) : MessageBase("quote:generated");
TradeExecutedEvent:
[MessageKey("trade:executed")]
public record TradeExecutedEvent(...) : MessageBase("trade:executed");
DealClosedEvent:
[MessageKey("deal:closed")]
public record DealClosedEvent(...) : MessageBase("deal:closed");
PositionChangedEvent:
[MessageKey("position:changed")]
public record PositionChangedEvent(...) : MessageBase("position:changed");
StrategySignalEvent:
[MessageKey("strategy:signal")]
public record StrategySignalEvent(...) : MessageBase("strategy:signal");
OrderCreatedEvent:
[MessageKey("order:created")]
public record OrderCreatedEvent(...) : MessageBase("order:created");
ChartUpdateEvent:
[MessageKey("chart:update")]
public record ChartUpdateEvent(...) : MessageBase("chart:update");
SystemStatusEvent:
[MessageKey("system:status")]
public record SystemStatusEvent(...) : MessageBase("system:status");Тогда в DatabaseBackgroundService вы можете подписаться на эти статические ключи.
В Program.cs вы регистрируете статические ключи с помощью:
var messageTypes = typeof(Program).Assembly.GetTypes()
.Where(t => typeof(IMessage).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface)
.Where(t => t.GetCustomAttribute<MessageKeyAttribute>() != null);
foreach (var type in messageTypes)
{
var attr = type.GetCustomAttribute<MessageKeyAttribute>();
if (attr != null)
{
var method = typeof(DynamicSubscriptionManager).GetMethod("RegisterStaticKey");
var genericMethod = method!.MakeGenericMethod(type);
genericMethod.Invoke(subscriptionManager, new object[] { attr.Key });
}
}Это зарегистрирует статический ключ из атрибута для каждого типа сообщения.
Но обратите внимание: в вашем коде есть также подписки по ключам, которые не используют типы сообщений, а используют строковые ключи.
Например, в QuotesConsoleService:
_subscriptionManager.Subscribe<QuoteGeneratedEvent>(
async (quote, ct) => { ... });Это использует статический ключ, зарегистрированный для QuoteGeneratedEvent.
Поэтому, если вы измените атрибут, то подписка будет работать.
Однако, если в каких-то местах используется подписка по строковому ключу, то нужно убедиться, что ключ совпадает.
Например, если где-то есть:
_subscriptionManager.Subscribe<QuoteGeneratedEvent>("quote:generated", ...);то это тоже будет работать, потому что ключ "quote:generated" зарегистрирован.
Но если вы измените атрибут на другой ключ, то подписка по старому строковому ключу не сработает.
Вывод:
Измените все события на статические ключи, убедившись, что атрибут [MessageKey] и MessageBase используют одну и ту же строку.
После этого в DatabaseBackgroundService вы можете использовать:
_subscriptionManager.RegisterStaticKey<TradeExecutedEvent>("trade:executed");
_subscriptionManager.Subscribe<TradeExecutedEvent>(async (tradeEvent, ct) => { ... });И это будет работать.
Если же вы хотите оставить динамические ключи, то в DatabaseBackgroundService вам нужно подписаться на каждый символ отдельно.
Например:
var symbols = new[] { "AAA", "BBB", ... };
foreach (var symbol in symbols)
{
_subscriptionManager.Subscribe<TradeExecutedEvent>($"trade:{symbol}", async (tradeEvent, ct) => { ... });
}Но это неудобно и неэффективно.
Поэтому я настоятельно рекомендую использовать статические ключи.
Давайте перепишем TradingEvents.cs на статические ключи.
Отличный вопрос! Давайте разберем на конкретных примерах.
Разница между статическим и динамическим ключами
Пример 1: Статический ключ
// Ключ ВСЕГДА "trade:executed" для ЛЮБОГО символа
[MessageKey("trade:executed")]
public record TradeExecutedEvent(
string Symbol, // "AAA"
string Side, // "Buy"
decimal Price, // 100.5
int Quantity, // 10
string StrategyName, // "Strategy_AAA"
DateTime Timestamp
) : MessageBase("trade:executed");
// ↑ Здесь жестко задано "trade:executed"Как работает:
Для символа "AAA": ключ =
"trade:executed"Для символа "BBB": ключ =
"trade:executed"Для символа "CCC": ключ =
"trade:executed"
Подписка:
// Подписываемся один раз и получаем ВСЕ сделки
_subscriptionManager.Subscribe<TradeExecutedEvent>(async (trade, ct) =>
{
// Здесь получим сделки для AAA, BBB, CCC и т.д.
Console.WriteLine($"Trade: {trade.Symbol} {trade.Side} {trade.Price}");
});Пример 2: Динамический ключ
// Ключ РАЗНЫЙ для каждого символа
[MessageKey("trade:executed:{Symbol}")]
public record TradeExecutedEvent(
string Symbol, // "AAA"
string Side, // "Buy"
decimal Price, // 100.5
int Quantity, // 10
string StrategyName, // "Strategy_AAA"
DateTime Timestamp
) : MessageBase($"trade:executed:{Symbol}");
// ↑ Здесь ключ строится динамическиКак работает:
Для символа "AAA": ключ =
"trade:executed:AAA"Для символа "BBB": ключ =
"trade:executed:BBB"Для символа "CCC": ключ =
"trade:executed:CCC"
Подписка:
// Нужно подписаться на КАЖДЫЙ символ отдельно
_subscriptionManager.Subscribe<TradeExecutedEvent>("trade:executed:AAA",
async (trade, ct) =>
{
// Только для AAA
Console.WriteLine($"Trade AAA: {trade.Side} {trade.Price}");
});
_subscriptionManager.Subscribe<TradeExecutedEvent>("trade:executed:BBB",
async (trade, ct) =>
{
// Только для BBB
Console.WriteLine($"Trade BBB: {trade.Side} {trade.Price}");
});Наглядное сравнение
Статический ключ ("trade:executed"):
EventHub
├── Ключ "trade:executed"
│ ├── Подписчик 1 (DatabaseBackgroundService)
│ ├── Подписчик 2 (TradingMonitorService)
│ └── Подписчик 3 (StatisticsService)
│
├── Событие: TradeExecutedEvent(Symbol="AAA", ...)
│ └→ Все подписчики получают
│
├── Событие: TradeExecutedEvent(Symbol="BBB", ...)
│ └→ Все подписчики получают
│
└── Событие: TradeExecutedEvent(Symbol="CCC", ...)
└→ Все подписчики получаютДинамический ключ ("trade:executed:{Symbol}"):
EventHub
├── Ключ "trade:executed:AAA"
│ └── Подписчик (только для AAA)
│
├── Ключ "trade:executed:BBB"
│ └── Подписчик (только для BBB)
│
├── Ключ "trade:executed:CCC"
│ └── Подписчик (только для CCC)
│
├── Событие: TradeExecutedEvent(Symbol="AAA", ...)
│ └→ Только подписчик на "trade:executed:AAA"
│
├── Событие: TradeExecutedEvent(Symbol="BBB", ...)
│ └→ Только подписчик на "trade:executed:BBB"
│
└── Событие: TradeExecutedEvent(Symbol="CCC", ...)
└→ Только подписчик на "trade:executed:CCC"Практические примеры из вашего проекта
1. QuoteGeneratedEvent (статический - хорошо):
// Так у вас сейчас:
[MessageKey("quote:generated")]
public record QuoteGeneratedEvent(
string Symbol, // "AAA"
decimal Open,
decimal High,
decimal Low,
decimal Close,
long Volume,
DateTime Timestamp
) : MessageBase("quote:generated");
// ↑ ОДИНАКОВЫЙ ключ для всех символов!2. PositionChangedEvent (динамический - плохо для вашего случая):
// Так у вас сейчас:
[MessageKey("position:changed")]
public record PositionChangedEvent(
string Symbol, // "AAA"
int NetQuantity,
decimal UnrealizedPnL,
string Status,
DateTime Timestamp
) : MessageBase($"position:{Symbol}");
// ↑ РАЗНЫЙ ключ для каждого символа!Проблема в вашем коде:
У вас смешаны подходы:
Атрибут
[MessageKey("trade:executed")]говорит: "это статический ключ"MessageBase
$"trade:{Symbol}"говорит: "нет, это динамический ключ"
Что делать с DatabaseBackgroundService?
Если оставить динамические ключи, вам придется:
// Узнать все символы заранее
var symbols = new[] { "AAA", "BBB", "CCC", ... };
// Подписаться на каждый
foreach (var symbol in symbols)
{
_subscriptionManager.Subscribe<TradeExecutedEvent>(
$"trade:{symbol}", // Динамический ключ
async (trade, ct) => { ... });
}Но проще сделать все ключи статическими (что я и рекомендую):
// Исправить ВСЕ события в TradingEvents.cs:
[MessageKey("trade:executed")]
public record TradeExecutedEvent(...) : MessageBase("trade:executed");
[MessageKey("deal:closed")]
public record DealClosedEvent(...) : MessageBase("deal:closed");
[MessageKey("position:changed")]
public record PositionChangedEvent(...) : MessageBase("position:changed");Почему для вашего проекта лучше статические ключи:
DatabaseBackgroundService должен получать ВСЕ сделки, позиции и т.д.
Не нужно знать список символов заранее
Проще отлаживать - все события одного типа имеют один ключ
Меньше кода - не нужно подписываться на каждый символ
Итоговое исправление:
Мы имеем два типа ключей: статические и динамические.
Статический ключ: это строка, которая не меняется от сообщения к сообщению.
Например, "quote:generated" для всех событий котировок, независимо от символа.Динамический ключ: это строка, которая может меняться в зависимости от данных сообщения.
Например, "quote:generated:AAPL" для котировок конкретного символа AAPL.Разница в написании:
Статический ключ: всегда одна и та же строка.
Динамический ключ: строится с использованием данных сообщения (например, символа).
Разница в действии:
Со статическим ключом все события одного типа имеют одинаковый ключ, и подписчик получает все события этого типа.
С динамическим ключом подписчик может подписаться на конкретный ключ (например, только на котировки AAPL) и получать только соответствующие события.
В вашем коде вы использовали атрибут [MessageKey] со статическим ключом, а в MessageBase передавали динамический ключ (с символом).
Это приводит к несоответствию: атрибут говорит одно, а фактический ключ (из MessageBase) - другое.В системе маршрутизации (CompositeKeyRouter) используется ключ из MessageBase (тот, который передается в конструктор).
Атрибут [MessageKey] используется для регистрации статического ключа, но если MessageBase создает другой ключ, то сообщение будет маршрутизировано по ключу из MessageBase.Пример:
[MessageKey("trade:executed")]
public record TradeExecutedEvent(...) : MessageBase($"trade:{Symbol}");При публикации TradeExecutedEvent для символа "AAA" будет создано сообщение с ключом "trade:AAA".
Но атрибут говорит, что статический ключ "trade:executed". Это разные ключи.Если вы подпишетесь на "trade:executed", то не получите это сообщение, потому что оно было опубликовано с ключом "trade:AAA".
Как исправить?
Вариант 1: Использовать статические ключи для всех событий.
Измените MessageBase на статический ключ, например: MessageBase("trade:executed")Вариант 2: Использовать динамические ключи и подписываться на них с помощью масок (если ваша система поддерживает).
Но в текущей реализации BusLibrary02, судя по коду, нет поддержки масок в ключах.Вариант 3: Оставить как есть, но подписываться на динамические ключи, предварительно зная символы.
Например, подписаться на "trade:AAA", "trade:BBB" и т.д. Но это неудобно, если символов много.Вариант 4: Использовать и статический, и динамический ключ одновременно. Но для этого нужно изменить систему маршрутизации.
Вариант 5: Использовать два отдельных механизма:
- статические ключи для глобальных подписок (через атрибуты)
- динамические ключи для точных подписок (через IDynamicSubscriptionManager)Однако, в текущей системе (CompositeKeyRouter) используется только один ключ для маршрутизации.
Вам нужно решить, какой подход вам нужен.
Рекомендую использовать статические ключи для всех событий, если только вам действительно не нужна фильтрация по символам на уровне маршрутизации.
Если нужна фильтрация, то можно использовать статический ключ и фильтровать в обработчике.
Пример статического ключа:
text[MessageKey("trade:executed")] public record TradeExecutedEvent( string Symbol, ... ) : MessageBase("trade:executed");Тогда в обработчике вы можете проверить Symbol и решить, обрабатывать ли это сообщение.
Пример динамического ключа (если вы хотите, чтобы разные символы шли разным подписчикам):
text[MessageKey("trade:executed:{Symbol}")] public record TradeExecutedEvent( string Symbol, ... ) : MessageBase($"trade:executed:{Symbol}");Но тогда подписчик должен знать символы, на которые он подписывается.
В вашем случае, если вы хотите, чтобы DatabaseBackgroundService получал все сделки, независимо от символа, то используйте статический ключ.
Если же вы хотите, чтобы были разные подписчики для разных символов, то используйте динамический ключ.
Учитывая, что вы хотите сохранять все в базу данных, скорее всего, вам нужны все сделки, поэтому статический ключ.
Однако, если вы планируете в будущем иметь отдельные базы данных для разных символов, то динамический ключ.
Но в текущей реализации InMemoryDatabase хранит все в одном месте, поэтому статический ключ предпочтительнее.
Давайте изменим ваши события на статические ключи.
Пример изменения для TradeExecutedEvent:
textБыло: [MessageKey("trade:executed")] public record TradeExecutedEvent(...) : MessageBase($"trade:{Symbol}"); Стало: [MessageKey("trade:executed")] public record TradeExecutedEvent(...) : MessageBase("trade:executed");Тогда в DatabaseBackgroundService вы можете подписаться на "trade:executed" и получать все сделки.
Аналогично для других событий.
Но обратите внимание: в вашем коде уже есть другие сервисы, которые подписываются на события.
Например, TradingMonitorService подписывается на TradeExecutedEvent, QuoteGeneratedEvent и PositionChangedEvent.Если вы измените ключи, то нужно убедиться, что все подписки используют правильные ключи.
В TradingMonitorService вы используете IDynamicSubscriptionManager.Subscribe<TradeExecutedEvent> (без указания ключа).
Этот метод использует статический ключ, зарегистрированный для типа TradeExecutedEvent (через атрибут или через RegisterStaticKey).Поэтому, если вы измените атрибут и MessageBase на статический ключ, то подписка в TradingMonitorService будет работать.
Итог:
Рекомендую для всех событий использовать статические ключи. Примеры:
textQuoteGeneratedEvent: [MessageKey("quote:generated")] public record QuoteGeneratedEvent(...) : MessageBase("quote:generated"); TradeExecutedEvent: [MessageKey("trade:executed")] public record TradeExecutedEvent(...) : MessageBase("trade:executed"); DealClosedEvent: [MessageKey("deal:closed")] public record DealClosedEvent(...) : MessageBase("deal:closed"); PositionChangedEvent: [MessageKey("position:changed")] public record PositionChangedEvent(...) : MessageBase("position:changed"); StrategySignalEvent: [MessageKey("strategy:signal")] public record StrategySignalEvent(...) : MessageBase("strategy:signal"); OrderCreatedEvent: [MessageKey("order:created")] public record OrderCreatedEvent(...) : MessageBase("order:created"); ChartUpdateEvent: [MessageKey("chart:update")] public record ChartUpdateEvent(...) : MessageBase("chart:update"); SystemStatusEvent: [MessageKey("system:status")] public record SystemStatusEvent(...) : MessageBase("system:status");Тогда в DatabaseBackgroundService вы можете подписаться на эти статические ключи.
В Program.cs вы регистрируете статические ключи с помощью:
textvar messageTypes = typeof(Program).Assembly.GetTypes() .Where(t => typeof(IMessage).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface) .Where(t => t.GetCustomAttribute<MessageKeyAttribute>() != null); foreach (var type in messageTypes) { var attr = type.GetCustomAttribute<MessageKeyAttribute>(); if (attr != null) { var method = typeof(DynamicSubscriptionManager).GetMethod("RegisterStaticKey"); var genericMethod = method!.MakeGenericMethod(type); genericMethod.Invoke(subscriptionManager, new object[] { attr.Key }); } }Это зарегистрирует статический ключ из атрибута для каждого типа сообщения.
Но обратите внимание: в вашем коде есть также подписки по ключам, которые не используют типы сообщений, а используют строковые ключи.
Например, в QuotesConsoleService:
text_subscriptionManager.Subscribe<QuoteGeneratedEvent>( async (quote, ct) => { ... });Это использует статический ключ, зарегистрированный для QuoteGeneratedEvent.
Поэтому, если вы измените атрибут, то подписка будет работать.
Однако, если в каких-то местах используется подписка по строковому ключу, то нужно убедиться, что ключ совпадает.
Например, если где-то есть:
text_subscriptionManager.Subscribe<QuoteGeneratedEvent>("quote:generated", ...);то это тоже будет работать, потому что ключ "quote:generated" зарегистрирован.
Но если вы измените атрибут на другой ключ, то подписка по старому строковому ключу не сработает.
Вывод:
Измените все события на статические ключи, убедившись, что атрибут [MessageKey] и MessageBase используют одну и ту же строку.После этого в DatabaseBackgroundService вы можете использовать:
text_subscriptionManager.RegisterStaticKey<TradeExecutedEvent>("trade:executed"); _subscriptionManager.Subscribe<TradeExecutedEvent>(async (tradeEvent, ct) => { ... });И это будет работать.
Если же вы хотите оставить динамические ключи, то в DatabaseBackgroundService вам нужно подписаться на каждый символ отдельно.
Например:
textvar symbols = new[] { "AAA", "BBB", ... }; foreach (var symbol in symbols) { _subscriptionManager.Subscribe<TradeExecutedEvent>($"trade:{symbol}", async (tradeEvent, ct) => { ... }); }Но это неудобно и неэффективно.
Поэтому я настоятельно рекомендую использовать статические ключи.
Давайте перепишем TradingEvents.cs на статические ключи.
Отличный вопрос! Давайте разберем на конкретных примерах.
Разница между статическим и динамическим ключами
Пример 1: Статический ключ
csharp// Ключ ВСЕГДА "trade:executed" для ЛЮБОГО символа [MessageKey("trade:executed")] public record TradeExecutedEvent( string Symbol, // "AAA" string Side, // "Buy" decimal Price, // 100.5 int Quantity, // 10 string StrategyName, // "Strategy_AAA" DateTime Timestamp ) : MessageBase("trade:executed"); // ↑ Здесь жестко задано "trade:executed"Как работает:
Для символа "AAA": ключ =
"trade:executed"Для символа "BBB": ключ =
"trade:executed"Для символа "CCC": ключ =
"trade:executed"Подписка:
csharp// Подписываемся один раз и получаем ВСЕ сделки _subscriptionManager.Subscribe<TradeExecutedEvent>(async (trade, ct) => { // Здесь получим сделки для AAA, BBB, CCC и т.д. Console.WriteLine($"Trade: {trade.Symbol} {trade.Side} {trade.Price}"); });Пример 2: Динамический ключ
csharp// Ключ РАЗНЫЙ для каждого символа [MessageKey("trade:executed:{Symbol}")] public record TradeExecutedEvent( string Symbol, // "AAA" string Side, // "Buy" decimal Price, // 100.5 int Quantity, // 10 string StrategyName, // "Strategy_AAA" DateTime Timestamp ) : MessageBase($"trade:executed:{Symbol}"); // ↑ Здесь ключ строится динамическиКак работает:
Для символа "AAA": ключ =
"trade:executed:AAA"Для символа "BBB": ключ =
"trade:executed:BBB"Для символа "CCC": ключ =
"trade:executed:CCC"Подписка:
csharp// Нужно подписаться на КАЖДЫЙ символ отдельно _subscriptionManager.Subscribe<TradeExecutedEvent>("trade:executed:AAA", async (trade, ct) => { // Только для AAA Console.WriteLine($"Trade AAA: {trade.Side} {trade.Price}"); }); _subscriptionManager.Subscribe<TradeExecutedEvent>("trade:executed:BBB", async (trade, ct) => { // Только для BBB Console.WriteLine($"Trade BBB: {trade.Side} {trade.Price}"); });Наглядное сравнение
Статический ключ (
"trade:executed"):textEventHub ├── Ключ "trade:executed" │ ├── Подписчик 1 (DatabaseBackgroundService) │ ├── Подписчик 2 (TradingMonitorService) │ └── Подписчик 3 (StatisticsService) │ ├── Событие: TradeExecutedEvent(Symbol="AAA", ...) │ └→ Все подписчики получают │ ├── Событие: TradeExecutedEvent(Symbol="BBB", ...) │ └→ Все подписчики получают │ └── Событие: TradeExecutedEvent(Symbol="CCC", ...) └→ Все подписчики получаютДинамический ключ (
"trade:executed:{Symbol}"):textEventHub ├── Ключ "trade:executed:AAA" │ └── Подписчик (только для AAA) │ ├── Ключ "trade:executed:BBB" │ └── Подписчик (только для BBB) │ ├── Ключ "trade:executed:CCC" │ └── Подписчик (только для CCC) │ ├── Событие: TradeExecutedEvent(Symbol="AAA", ...) │ └→ Только подписчик на "trade:executed:AAA" │ ├── Событие: TradeExecutedEvent(Symbol="BBB", ...) │ └→ Только подписчик на "trade:executed:BBB" │ └── Событие: TradeExecutedEvent(Symbol="CCC", ...) └→ Только подписчик на "trade:executed:CCC"Практические примеры из вашего проекта
1. QuoteGeneratedEvent (статический - хорошо):
csharp// Так у вас сейчас: [MessageKey("quote:generated")] public record QuoteGeneratedEvent( string Symbol, // "AAA" decimal Open, decimal High, decimal Low, decimal Close, long Volume, DateTime Timestamp ) : MessageBase("quote:generated"); // ↑ ОДИНАКОВЫЙ ключ для всех символов!2. PositionChangedEvent (динамический - плохо для вашего случая):
csharp// Так у вас сейчас: [MessageKey("position:changed")] public record PositionChangedEvent( string Symbol, // "AAA" int NetQuantity, decimal UnrealizedPnL, string Status, DateTime Timestamp ) : MessageBase($"position:{Symbol}"); // ↑ РАЗНЫЙ ключ для каждого символа!Проблема в вашем коде:
У вас смешаны подходы:
Атрибут
[MessageKey("trade:executed")]говорит: "это статический ключ"MessageBase
$"trade:{Symbol}"говорит: "нет, это динамический ключ"Что делать с DatabaseBackgroundService?
Если оставить динамические ключи, вам придется:
csharp// Узнать все символы заранее var symbols = new[] { "AAA", "BBB", "CCC", ... }; // Подписаться на каждый foreach (var symbol in symbols) { _subscriptionManager.Subscribe<TradeExecutedEvent>( $"trade:{symbol}", // Динамический ключ async (trade, ct) => { ... }); }Но проще сделать все ключи статическими (что я и рекомендую):
csharp// Исправить ВСЕ события в TradingEvents.cs: [MessageKey("trade:executed")] public record TradeExecutedEvent(...) : MessageBase("trade:executed"); [MessageKey("deal:closed")] public record DealClosedEvent(...) : MessageBase("deal:closed"); [MessageKey("position:changed")] public record PositionChangedEvent(...) : MessageBase("position:changed");Почему для вашего проекта лучше статические ключи:
DatabaseBackgroundService должен получать ВСЕ сделки, позиции и т.д.
Не нужно знать список символов заранее
Проще отлаживать - все события одного типа имеют один ключ
Меньше кода - не нужно подписываться на каждый символ
Итоговое исправление:
csharp// Events/TradingEvents.cs - СДЕЛАТЬ ВСЕ КЛЮЧИ СТАТИЧЕСКИМИ: [MessageKey("quote:generated")] public record QuoteGeneratedEvent(...) : MessageBase("quote:generated"); [MessageKey("trade:executed")] public record TradeExecutedEvent(...) : MessageBase("trade:executed"); [MessageKey("deal:closed")] public record DealClosedEvent(...) : MessageBase("deal:closed"); [MessageKey("position:changed")] public record PositionChangedEvent(...) : MessageBase("position:changed"); // И так далее для всех событий...Теперь DatabaseBackgroundService будет работать правильно с одним подписчиком на каждый тип события!
Динамические ключи нужны только если:
Разные обработчики для разных символов
Хотите изолировать обработку
Есть специфичная логика для конкретных символов
В вашем случае все события нужно сохранять в одну БД, поэтому статические ключи - лучший выбор.
Комментариев нет:
Отправить комментарий