Skip to content

Architecture

The library is split into 4 projects with a strict dependency direction: Base → Core, Data → Base → Core, Util → Core + Base. Nothing in Core references Base. This is enforced by directory-level structure and is what makes the trait/concrete split possible.

Project map

flowchart TB
  subgraph "Virtufin.Core (traits, no domain)"
    Core_EAV[EAV abstractions<br/>IEntity, IAttribute,<br/>IValueProvider, Registry]
    Core_Traits[Financial contract traits<br/>ITradeAction, ITradeEvent,<br/>IExecutionState]
    Core_Decimal[DecimalAmount<br/>arbitrary-precision decimal]
    Core_Position[Position trait<br/>IPosition, IQuantity]
  end

  subgraph "Virtufin.Base (concrete domain)"
    Base_Currency[Currency, Cash, Stock<br/>PositionSize, Transaction]
    Base_Scenario[Scenario, ScenarioHistory]
    Base_Contract[ContractIdentifier, UTI,<br/>Signatory, AssetIdentifier]
    Base_Exec[Executors<br/>Deterministic, Slippage,<br/>Live, ObserveOnly]
  end

  subgraph "Virtufin.Data (serialization + reactive)"
    Data_Variant[Variant discriminated union]
    Data_FB[FlatBuffers<br/>schema + codegen]
    Data_NATS[NATS observable]
    Data_WS[WebSocket observable]
    Data_VK[Valkey observable]
  end

  subgraph "Virtufin.Util"
    Util_Time[Time, TimeStamp]
    Util_Fmt[Formatters]
  end

  Base_Currency --> Core_Decimal
  Base_Scenario --> Core_EAV
  Base_Contract --> Core_Traits
  Base_Exec --> Core_Traits
  Data_Variant --> Core_Decimal
  Data_FB --> Data_Variant
  Data_NATS --> Core_EAV
  Util_Time --> Core_EAV

The trait/concrete split

The Core project defines traits (interfaces and types that don't depend on domain primitives). The Base project provides concrete implementations of those traits.

Core (trait) Base (concrete)
ITradeAction (marker) RichTradeAction ADT (BuyOrder, SellOrder, CancelOrder, Transfer, Interest, WithdrawInterest)
ITradeEvent<A> (with A Action property) TradeEvent ADT (OrderPlaced, OrderCancelled, OrderExpired, FillReceived, ExecutionCleared, ...)
IExecutionState (marker) DeterministicState (empty), StochasticState (seeded PRNG)
Position (record) PositionSize (concrete), PositionTransferContract (concrete)
IQuantity (trait) DecimalAmountQuantity, SignedDecimalAmountQuantity

Why split this way? Two reasons:

  1. Substitutability: a function declared as IExecutor<TState, ITradeAction, ITradeEvent<ITradeAction>> is callable with any IExecutor<TState, RichTradeAction, TradeEvent> (because RichTradeAction : ITradeAction and TradeEvent : ITradeEvent<RichTradeAction>, and ITradeEvent<out A> is covariant).
  2. Testability: tests can supply a minimal IQuantity stub without depending on DecimalAmount-backed types.

The convention is enforced by direction: Base may import Core, but Core never imports Base. Code review at the PR level catches violations.

The Variant cross-cutting type

Variant lives in Virtufin.Data but uses DecimalAmount from Core and IQuantity from Core for the numeric cases. It is the lingua franca for the platform: code in WorkManager, WebSocketManager, API Gateway, and external workers all produce and consume Variant values over the wire.

The FlatBuffers schema in Virtufin.Data lets a Variant produced in C# be deserialized in Python and TypeScript with the same shape — see Variant Type.

Reactive layer

Virtufin.Data provides three IObservable<T> adapters, all with auto-reconnect:

Adapter Source Notes
NatsObservable NATS subjects Subject-pattern match, re-subscribes on reconnect
WebSocketObservable WebSocket frames Reconnects with exponential backoff
ValkeyObservable Valkey keyspace notifications Key-pattern match, re-subscribes on PSUB reconnect

All three are documented under Observables.

The execution algebra

IExecutor<TState, TAction, TEvent> is the algebra of "given a current state and an action, produce the next state and an outcome event." The library provides four implementations:

Class Algorithm shape When to use
DeterministicExecutorBase<TState, TAction, TEvent> ExecuteProcess(state, action) Backtests, unit tests, deterministic simulations
SlippageExecutorBase<TState, TAction, TEvent> ExecuteCalculateSlippage(state, action)Process(state, action, slippage) Realistic paper trading with slippage modeling
LiveExecutorBase<TState, TAction, TEvent> ExecuteProcess(state, action) Production exchange routing (subclass)
ObserveOnlyExecutor<TState, TAction, TEvent> Decorator — wraps another IExecutor and records events SHADOW_* scenarios

All three base classes are abstract — subclasses define the case-by-case fill logic. The bases differ in algorithm shape, not in name or config field. See DecimalAmount for the numeric type used in fills.

NativeAOT compatibility

The library is built to be NativeAOT-compatible: - All public APIs are AOT-friendly (no reflection on user types, no late binding) - JSON serialization uses source-generated JsonSerializerContexts where applicable - FlatBuffers serialization is codegen, not reflection

If you're consuming this library from an AOT-published project, the Variant and DecimalAmount types work without any AOT warnings. Subclassing IExecutor from an AOT project requires care with the case-by-case pattern matching — DynamicallyAccessedMembers annotations may be needed on the TAction type parameter.

Project file conventions

Each project follows these conventions: - <TargetFramework>net10.0</TargetFramework> - <Nullable>enable</Nullable> - <ImplicitUsings>enable</ImplicitUsings> - <LangVersion>preview</LangVersion> (C# 14 preview features) - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> (CI enforces clean builds) - Line width 80 characters, Allman braces, 4-space indent - File-scoped namespaces preferred

Package versions are managed centrally in Directory.Packages.props. No <Version> elements in .csproj files.