Behaviour Layer
The Virtufin.Core.Behaviour namespace defines the coalgebra shapes
that model everything in the trading loop that evolves over time —
strategies, execution engines — plus the algebra shapes that derive
values from event logs. The split mirrors the domain spec: behaviour
layer (processes that step) vs derivation layer (values computed from
logs).
IProcess — the generic coalgebra
A stateful process is modelled as (State, Input) → (State, Output) with
the hidden state exposed explicitly, never captured in a closure:
public interface IProcess<TState, TInput, TOutput>
{
TState Initial { get; }
(TState NextState, TOutput Output) Step(TState state, TInput input);
}
Two domain instantiations exist:
| Interface | Role | Shape |
|---|---|---|
IDecide<TState, TMarket, TPortfolio, TAction> |
decide — the strategy |
S × (M, P) → (S, A[]) |
IExecutor<TState, TAction, TEvent> |
execute — the execution engine |
S × A → (S, E) |
IDecide — the strategy trait
IDecide specialises IProcess for the trading loop: given the
strategy's hidden state, a market observation, and the folded portfolio
state, produce the next state and zero or more trade actions.
public interface IDecide<TState, TMarket, TPortfolio, TAction>
: IProcess<TState, (TMarket, TPortfolio), TAction[]>
where TState : IStrategyState
where TMarket : IMarketEvent
where TPortfolio : IPortfolioState
where TAction : ITradeAction
{
}
TState : IStrategyState— hidden indicator state (moving averages, model weights, cooldowns).IStrategyStateis the marker counterpart ofIExecutionStateon the executor side.TMarket : IMarketEvent— the market observation; covers both raw feed events (TickReceived,OrderBookReceived) and derived signals (RichMarketEvent: candles, VWAP, volatility).TPortfolio : IPortfolioState— the folded holdings a strategy observes, produced by foldingIPositionEvents with anIAlgebra<TEvent, TState>.TAction : ITradeAction— the emitted intent; the output isTAction[]because one observation may yield zero or more actions.
Worked example
public sealed record MaState(
decimal FastMa, decimal SlowMa) : IStrategyState;
public sealed class MaCrossover
: IDecide<MaState, RichMarketEvent, MyPortfolio, TradeAction>
{
public MaState Initial => new(0m, 0m);
public (MaState, TradeAction[]) Step(
MaState state,
(RichMarketEvent, MyPortfolio) input)
{
var (market, portfolio) = input;
var next = UpdateMas(state, market);
if (!CrossedUp(state, next) || portfolio.IsLong)
{
return (next, []);
}
return (next, [new TradeAction.BuyOrder(/* ... */)]);
}
}
Because IDecide is an IProcess, any strategy composes with the
generic drivers below — and with the IExecutor pipeline, since both
halves of the loop share the same coalgebra shape.
Driving a process: Run
ProcessExtensions.Run folds a process over an input stream — the
scan combinator of the domain spec:
// decide driven by a stream of (market, portfolio) observations
IObservable<(MaState State, TradeAction[] Output)> behaviour =
strategy.Run(inputs);
// or resume from a checkpoint:
IObservable<(MaState, TradeAction[])> resumed =
strategy.Run(inputs, initialState: checkpoint);
- One
(state, output)pair is emitted per input, in order. - The state is fresh per subscription — resubscribing restarts from
Initial(or the supplied checkpoint), so a cold source replays deterministically. - The coalgebra itself stays pure and pull-based; only the driver is
reactive. Synchronous callers (tests, batch backtests) can call
Stepdirectly.
The trading loop
decide and execute pair into a corecursive loop — fills become
position events that feed the next decision:
flowchart LR
MKT[("Market stream\nIObservable<M>")]
PORT[("Portfolio state\nfolded from PositionEvents")]
DEC["IDecide<S, M, P, A>\nStep(s, (m, p)) -> (s', a[])"]
RUN["Run\nscan over inputs"]
EXE["IExecutor<S, A, E>\nExecute(s, a) -> (s', e)"]
ALG[("PositionAlgebra\nfolds fills")]
MKT --> RUN
PORT --> RUN
RUN --> DEC
DEC -->|"TradeAction[]"| EXE
EXE -->|"TradeEvent"| ALG
ALG -->|"feedback"| PORT
The feedback edge is the defining feature: the portfolio input to
decide is itself an output of the loop, making the system a greatest
fixed point of its behavioural equations.
Derivation layer
Complementing the processes, the derivation layer computes values from event logs:
| Shape | Signature | Purpose |
|---|---|---|
IAlgebra<TEvent, TState> |
Apply(state, event) : TState |
F-algebra — fold an event log into state |
ISignal<T> |
T Value { get; } |
A continuously derivable value (position, P&L) |
These are the same concepts the IExecutor algebra table in
Architecture builds on — executors behave as
coalgebras, strategies as IDecide, and both compose through the loop
above. The ratified, language-agnostic version of this model lives in
the behaviour spec in the
virtufin-openspec
repository.