Skip to content

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). IStrategyState is the marker counterpart of IExecutionState on 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 folding IPositionEvents with an IAlgebra<TEvent, TState>.
  • TAction : ITradeAction — the emitted intent; the output is TAction[] 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 Step directly.

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.