Давайте попробуем разработать Архитектуру Программной Системы по Торговле Акциями и другими Инструментами.
C#, .NetCore, Aspire, RabbitMQ, Docker, Desktop
Схема взаимодействия будет такая
- Я называю сущность, делаю краткий комментарий на нее , а ты уже создаешь правильным языком описание.
Для начала мы торгуем Линейные Активы - Акции, Фьючерсы на акции
ТОргуем мы на Miceх
Начнем с Таких Торговых сущностей.
Тicker -> TradeStrtategy
Portfolio -> Tickers
Не знаю Как надо обозначать схему One to Many.
Портфель торгует Tickers
Ticker Торгует свои Strategies
--------
В обычных торговых системах в Портфолио торгуют одну Strategy для всехTickers
У нас же может торговать Ticker несколько своих Strategies
То есть в одном ПОртфеле может быть записи в БД
TickerA , StrategyX1
TickerA , StrategyX2
TickerA , StrategyX3
TickerB , StrategyY1
TickerC , StrategyX1
TickerC , StrategyY1
---------------------------
ТО есть конкретная СТратегия не привязана к конкретному Ticker.
STrategy - свободна для выбора любым Tickers
------------------------------
Теперь напиши все, что я сейчас написал нормальным программным, архитектурным и торговым языком. С правильной Терминологией
Посмотрим что у нас получиться.
Алиса
Архитектурное описание системы алгоритмической торговли
Технологический стек
Платформа: .NET (C#), .NET Aspire — оркестрация микросервисов
Брокер сообщений: RabbitMQ — асинхронный обмен событиями между сервисами
Контейнеризация: Docker / Docker Compose
Клиент: десктоп-приложение (WPF / WinUI)
Биржа: Московская биржа (MOEX) — биржевой интерфейс через FIX/FAST или REST API
Класс торгуемых инструментов
Линейные инструменты — инструменты с симметричным профилем выплат, где P&L изменяется линейно относительно цены базового актива. В текущей конфигурации системы торгуются:
Акции (equities) — долевые ценные бумаги
Фьючерсы на акции (single-stock futures) — производные финансовые инструменты на отдельные акции
Доменная модель: ключевые сущности и связи
Сущности
Сущность Описание
Portfolio Торговый портфель — логический контейнер, объединяющий торгуемые инструменты под единым управлением рисками и капиталом. Определяет границы доступного капитала, лимиты и правила управления позицией.
Ticker Торгуемый инструмент — конкретный биржевой символ (тикер), например GAZP, SBER, или фьючерсный контракт, например SRZ5. Привязан к биржевому инструменту с уникальным идентификатором (ISIN / ticker / secid).
TradeStrategy Торговая стратегия — алгоритм принятия торговых решений. Инкапсулирует логику генерации торговых сигналов, правила входа/выхода, параметры управления позицией. Не привязана к конкретному инструменту и является переиспользуемым компонентом.
Структура связей
Связь между сущностями в стандартных торговых системах и в проектируемой системе принципиально различается:
Стандартная модель (типичная):
text
Portfolio ──1:N──> Strategy ──1:N──> Ticker
Один портфель → одна стратегия на весь набор тикеров. Стратегия жёстко привязана к портфелю и применяется ко всем входящим в него тикерам одинаково.
Проектируемая модель:
text
Portfolio ──1:N──> Ticker
Ticker ──N:M──> TradeStrategy (опосредованная связь через Portfolio)
Каждый тикер внутри портфеля может торговаться несколькими независимыми стратегиями, а каждая стратегия может применяться к разным тикерам — в том числе в рамках одного портфеля.
Формализация связи
Связь «тикер ↔ стратегия» в рамках портфеля представляет собой ассоциативную сущность (junction entity), которая материализует отношение «многие-ко-многим» между Ticker и TradeStrategy, ограниченное контекстом Portfolio:
text
PortfolioTickerStrategy
├── PortfolioId (FK → Portfolio)
├── TickerId (FK → Ticker)
├── StrategyId (FK → TradeStrategy)
└── (composite key: PortfolioId + TickerId + StrategyId)
Каждая строка этой таблицы — самостоятельная торговая связка: конкретный тикер, управляемый конкретной стратегией, в рамках конкретного портфеля.
Пример данных
Portfolio Ticker Strategy
Portfolio A TickerA StrategyX1
Portfolio A TickerA StrategyX2
Portfolio A TickerA StrategyX3
Portfolio A TickerB StrategyY1
Portfolio A TickerC StrategyX1
Portfolio A TickerC StrategyY1
Из таблицы видно: StrategyX1 применяется и к TickerA, и к TickerC — стратегия не инстанцируется под конкретный тикер, а разделяется между ними.
Ключевое архитектурное отличие
Стратегия TradeStrategy является инструментно-независимой (instrument-agnostic). Она не содержит ссылки на конкретный тикер в своей внутренней конфигурации, а получает инструмент в качестве параметра исполнения в момент связки с тикером через PortfolioTickerStrategy. Это позволяет:
Переиспользовать одну стратегию для разных инструментов без дублирования кода или конфигурации.
Комбинировать несколько стратегий на одном тикере — каждая генерирует независимый поток торговых сигналов, которые могут агрегироваться или исполняться независимо.
Гибко перераспределять капитал портфеля между связками «тикер–стратегия».
Что дальше — опишите следующую сущность, и я продолжу формализацию модели.
-----------------
Тут как раз и кроется ключевой момент: на уровне «чистых» сущностей Ticker и TradeStrategy у тебя Many‑to‑Many, а не One‑to‑Many.
Почему это Many‑to‑Many
Посмотри на твои примеры:
TickerA → StrategyX, StrategyY, StrategyZ (один тикер → много стратегий)
StrategyX → TickerA, TickerB (одна стратегия → много тикеров)
Это и есть определение связи Many‑to‑Many: каждый тикер может работать с несколькими стратегиями, и каждая стратегия может работать с несколькими тикерами.
Где возникает иллюзия One‑to‑Many
Иллюзия One‑to‑Many появляется, когда ты смотришь только с одной стороны:
«У тикера A есть 3 стратегии» → кажется, что это Ticker → Strategies (One‑to‑Many).
Но если посмотреть на стратегию X: она работает и на A, и на B → значит, это уже не One‑to‑Many, а Many‑to‑Many.
One‑to‑Many было бы, если бы стратегия «принадлежала» тикеру и не могла использоваться нигде ещё. В твоём случае стратегия — общий, переиспользуемый компонент, поэтому связь Many‑to‑Many.
Как это правильно моделировать в архитектуре и БД
В предметной области (Domain)
Ты не делаешь public List<TradeStrategy> Strategies прямо в классе Ticker. Вместо этого ты вводишь агрегацию в контексте портфеля — ту самую ассоциативную сущность:
csharp
public class PortfolioTickerStrategy
{
public Guid PortfolioId { get; set; }
public Guid TickerId { get; set; }
public Guid StrategyId { get; set; }
// дополнительные параметры связки: вес, лимит, приоритет и т.п.
public decimal Weight { get; set; }
}
Теперь связь материализуется не между Ticker и Strategy напрямую, а через эту таблицу. Это и есть канонический способ реализации Many‑to‑Many в реляционной модели.
В коде (C#)
В доменных сущностях ты не хранишь списки друг в друге:
csharp
public class Ticker
{
public Guid Id { get; set; }
public string Symbol { get; set; }
// нет списка стратегий здесь
}
public class TradeStrategy
{
public Guid Id { get; set; }
public string Name { get; set; }
// нет списка тикеров здесь
}
А «список стратегий для тикера в портфеле» ты получаешь через репозиторий/сервис:
csharp
var links = _portfolioTickerStrategyRepo
.GetForPortfolioAndTicker(portfolioId, tickerId);
Это даёт гибкость: один и тот же TradeStrategy можно использовать в разных портфелях, на разных тикерах, с разными весами и лимитами.
Важное уточнение про контекст «в рамках портфеля»
Если ты попытаешься сделать One‑to‑Many (например, Ticker.Strategies), ты потеряешь возможность:
Применять одну стратегию к разным тикерам без дублирования конфигурации.
Менять состав связок «тикер–стратегия» независимо для каждого портфеля.
Назначать разные параметры (вес, лимит) для одной и той же стратегии на разных тикерах.
А у тебя как раз есть требование: «Стратегия свободна для выбора любым тикером» — это и есть Many‑to‑Many.
Как это выглядит в терминах .NET Aspire и микросервисов
Сервис стратегий хранит определение TradeStrategy (логика, параметры), не зная про тикеры.
Сервис портфелей/позиций хранит PortfolioTickerStrategy — именно он «знает», какие стратегии на каких тикерах работают в данном портфеле.
Торговый движок читает эти связки, для каждой пары Ticker + Strategy запускает отдельный экземпляр логики стратегии (или переиспользует её с контекстом тикера).
Практический совет под твой стек (C#, .NET Aspire, RabbitMQ)
Учитывая, что ты делаешь асинхронный сервис на TPL Dataflow и RabbitMQ:
Событие «новая связка»: PortfolioTickerStrategyCreated → RabbitMQ.
Consumer в торговом движке: создаёт/обновляет поток обработки для этой связки (Dataflow block на пару TickerId + StrategyId).
Событие «удаление связки»: останавливает соответствующий блок.
Так ты сохраняешь Many‑to‑Many на уровне данных и при этом можешь иметь отдельные «контексты исполнения» для каждой связки.
Если скажешь, какую следующую сущность хочешь описать (например, Order, Position, Execution), я сразу дам её формализацию в этой же модели.
---------
Да, в коде это эквивалентно: убрав ссылки Ticker → Strategy и Strategy → Ticker, ты получаешь ровно ту же модель Many‑to‑Many, только связь теперь живёт вне обеих сущностей — в ассоциативной таблице. Смысл не меняется, меняется только точка сборки.
А вот вопрос «что торгует портфель» — ключевой, и ответ на него нетривиальный.
Что является атомарной торговой единицей
Торговое решение рождается только в моменте Strategy × Ticker. Ни один из элементов по отдельности не порождает сделку:
Стратегия без тикера — алгоритм без входных данных, бесполезен.
Тикер без стратегии — просто рыночные данные, никто не принимает решение.
Сделка рождается только когда конкретная стратегия обрабатывает конкретный тикер.
Значит, атомарная торговая единица — это пара (Ticker, Strategy). Третий вариант.
text
Portfolio ──1:N──> TickerStrategyBinding(Ticker, Strategy)
Но это не вся картина
Есть три разных среза, и в каждом «единица торговли» — разная:
Срез Единица Пример
Торговое решение (signal generation) (Ticker, Strategy) — связка «StrategyX1 на GAZP дала сигнал Buy»
Рыночная позиция (position в стакане биржи) Ticker — агрегированная «Суммарная позиция по GAZP = +1000 лотов» (сумма по всем стратегиям на GAZP)
Attribution P&L (разбор результата) (Ticker, Strategy) — связка «StrategyX1 на GAZP заработала +5000, StrategyY1 на GAZP потеряла -2000»
Отсюда — что торгует портфель
Портфель торгует связками (Ticker, Strategy).
Не тикерами. Не стратегиями. Именно связками.
Потому что:
Если сказать «портфель торгует тикерами» — то непонятно, какой результат на тикере принесла каждая стратегия (а у тебя их несколько на одном тикере).
Если сказать «портфель торгует стратегиями» — то непонятно, на каком инструменте эта стратегия работает (а она работает на нескольких).
Связка (Ticker, Strategy) — единственный объект, для которого можно однозначно посчитать P&L, оценить риск, назначить лимит.
Как это ложится в архитектуру
csharp
// Атомарная торговая единица — то, что портфель "торгует"
public class TickerStrategyBinding
{
public Guid Id { get; set; }
public Guid PortfolioId { get; set; } // FK → Portfolio
public Guid TickerId { get; set; } // FK → Ticker
public Guid StrategyId { get; set; } // FK → TradeStrategy
// Параметры именно этой связки
public decimal CapitalAllocation { get; set; } // выделенный капитал
public int MaxPosition { get; set; } // лимит позиции
public bool IsActive { get; set; } // включена/выключена
}
// Рыночная позиция — агрегат по тикеру (результат торговли)
public class Position
{
public Guid PortfolioId { get; set; }
public Guid TickerId { get; set; }
public decimal NetQty { get; set; } // суммарная позиция по тикеру
public decimal AvgPrice { get; set; } // средняя цена
}
// P&L attribution — результат по связке
public class StrategyPnL
{
public Guid BindingId { get; set; } // FK → TickerStrategyBinding
public decimal RealizedPnL { get; set; }
public decimal UnrealizedPnL { get; set; }
}
Аналогия из жизни
Представь, что портфель — это инвестиционный фонд, тикеры — инструменты на рынке, стратегии — трейдеры. Ты не говоришь «фонд торгует трейдерами» или «фонд торгует инструментами». Ты говоришь: «фонд торгует через трейдера X на инструменте Y». Связка (трейдер, инструмент) — это торговый мандат. Так же и здесь: TickerStrategyBinding — это торговый мандат внутри портфеля.
--------------
Да, именно так. Каждая строка портфеля — это торговый мандат (Ticker, Strategy) с привязанным к нему P&L. И вот что важно: по одному и тому же тикеру может быть несколько строк с разными P&L, потому что каждая стратегия работает независимо.
Формализация: атрибуция результатов по портфелю
Структура строки портфеля
Portfolio Ticker Strategy Realized P&L Unrealized P&L Net Position
Portfolio A SBER StrategyX +100 +20 +500
Portfolio A SBER StrategyY +140 −30 +200
Portfolio A GAZP StrategyY −200 +10 −1000
Portfolio A GAZP StrategyZ +300 −50 +800
Ключевые наблюдения
1. P&L атрибутируется на связку, а не на тикер.
Суммарный P&L по SBER = +100 + 140 = +240 — это агрегат, а не первичная единица учёта. Первичная единица — строка (SBER, StrategyX) = +100.
2. Позиция по тикеру — тоже агрегат.
Суммарная позиция по SBER = 500 + 200 = +700 лотов. Биржа видит одну позицию по SBER, не знает про стратегии. Но внутри системы ты знаешь, что из этих 700 лотов 500 поставила StrategyX, а 200 — StrategyY.
3. Одна стратегия на разных тикерах — независимые P&L.
StrategyY на SBER заработала +140, а на GAZP потеряла −200. Это два независимых результата, хотя стратегия одна и та же. Алгоритм идентичный, но рыночные данные и исполнение разные — значит и результат разный.
Что это значит для архитектуры
text
Portfolio (1)
└── TickerStrategyBinding (N) ← строка портфеля, атомарная единица
├── Ticker ← FK
├── Strategy ← FK
├── RealizedPnL ← накопленный реализованный P&L
├── UnrealizedPnL ← нереализованный P&L (mark-to-market)
├── NetPosition ← текущая позиция по этой связке
├── CapitalAllocation ← выделенный капитал
└── MaxPosition ← лимит позиции для этой связки
└── Position (агрегат по тикеру, не хранится, вычисляется)
├── TickerId
├── NetQty = Σ(NetPosition по всем связкам этого тикера)
└── AvgPrice = средневзвешенная по исполнениям
В коде
csharp
public class TickerStrategyBinding
{
public Guid Id { get; set; }
public Guid PortfolioId { get; set; }
public Guid TickerId { get; set; }
public Guid StrategyId { get; set; }
// Торговые параметры связки
public decimal CapitalAllocation { get; set; }
public int MaxPosition { get; set; }
public bool IsActive { get; set; }
// Текущее состояние связки
public int NetPosition { get; set; }
public decimal AvgEntryPrice { get; set; }
public decimal RealizedPnL { get; set; }
public decimal UnrealizedPnL { get; set; }
// Навигационные свойства
public Portfolio Portfolio { get; set; }
public Ticker Ticker { get; set; }
public TradeStrategy Strategy { get; set; }
}
Агрегированная позиция по тикеру — вычисляется, не хранится
csharp
public class TickerPositionAggregate
{
public Guid PortfolioId { get; set; }
public Guid TickerId { get; set; }
// Вычисляется как Σ по всем связкам этого тикера в портфеле
public int NetQty { get; set; }
public decimal TotalRealizedPnL { get; set; }
public decimal TotalUnrealizedPnL { get; set; }
// Вычисляется на лету, не хранится в БД как отдельная строка
public static TickerPositionAggregate From(
Guid portfolioId,
Guid tickerId,
IEnumerable<TickerStrategyBinding> bindings)
{
var relevant = bindings
.Where(b => b.PortfolioId == portfolioId
&& b.TickerId == tickerId);
return new TickerPositionAggregate
{
PortfolioId = portfolioId,
TickerId = tickerId,
NetQty = relevant.Sum(b => b.NetPosition),
TotalRealizedPnL = relevant.Sum(b => b.RealizedPnL),
TotalUnrealizedPnL = relevant.Sum(b => b.UnrealizedPnL),
};
}
}
Почему позиция по тикеру — вычисляемая, а не хранимая
Если хранить Position по тикеру отдельно, возникает проблема синхронизации: при каждой сделке придётся обновлять и TickerStrategyBinding, и Position. Два источника истины — рано или поздно они разойдутся. Правильнее хранить состояние на уровне связки (атомарном), а тикерную позицию получать агрегацией.
-------------------------------------
Что дальше — переходим к Order и Execution (жизненный цикл заявки и исполнение), или к Position (управление позицией и пересчёт P&L)?
--------------