Skip to content

Strategy Author Guide

Build a strategy worker in three steps: implement IDecide, subclass StrategyWorkerBase, and wire per-instance config.

1. Implement IDecide

Your strategy is a pure coalgebra — no CloudEvents, no Rx, no I/O. The hidden state is an explicit record; Step is a pure function.

using Virtufin.Core.Events.Trade;
using Virtufin.Strategy.DevKit;
using Virtufin.Strategy.DevKit.Events;

public sealed record RsiState(decimal AvgGain, decimal AvgLoss) : IStrategyState;

public sealed class RsiStrategy : IDecide<RsiState, RichMarketEvent, PortfolioState, TradeAction>
{
    public RsiState Initial => new(0m, 0m);

    public (RsiState, TradeAction[]) Step(RsiState state, (RichMarketEvent, PortfolioState) input)
    {
        var (market, portfolio) = input;
        if (market is not RichMarketEvent.CandleClosed candle)
        {
            return (state, []); // only candle closes move this strategy
        }
        // ... compute RSI, consult portfolio.Quantity(candle.Symbol) ...
        var next = state;
        var actions = new TradeAction[] { /* BuyOrder / SellOrder */ };
        return (next, actions);
    }
}

Constraints enforce the domain: TMarket : IMarketEvent, TPortfolio : IPortfolioState, TAction : ITradeAction. Emitting zero actions is normal (a no-op step).

2. Subclass StrategyWorkerBase

public sealed class RsiWorker : StrategyWorkerBase<RsiState, RichMarketEvent, PortfolioState, TradeAction>
{
    public RsiWorker() : base(
        new Uri("urn:virtufin:worker:rsi"),
        "rsi.response",
        new RsiStrategy(),
        new PortfolioAlgebra())
    {
    }

    protected override JsonObject EncodeAction(CloudEvent input, TradeAction action)
        => TradeActionJson.Encode(TradeActionEnricher.Enrich(
            action,
            GetStrategyId(input),
            new Venue(ConfigResolution.ResolveString(input, "venue", "binance-spot")),
            TimeSpan.FromSeconds(1)));
}

The base class:

  • folds virtufin.position.* events into the portfolio (no response),
  • steps virtufin.market.* events through Step,
  • skips market events whose mapped type does not fit TMarket,
  • encodes emitted actions into {"command":"trade","success":true, "actions":[...],"count":n}.

Override OnMarketEvent to react to config changes (e.g. re-window an indicator) before each step.

3. Per-instance config

CreateWorkerRequest.config entries are stamped onto every triggering CloudEvent as extension attributes. Read them with ConfigResolution.* (extension-only — same-named payload fields are ignored, because strategy payloads are foreign domain schemas).

protected override RsiState OnMarketEvent(CloudEvent input, RsiState state)
{
    var period = ConfigResolution.ResolvePositiveInt(input, "rsiperiod", 14);
    return state.Period == period ? state : state with { Period = period };
}

Note: CloudEvents extension attribute names cannot contain underscores (the CloudNative SDK and the WorkManager's config stamping both validate). Use a no-underscore spelling for your config keys.

See Bridge for the routing architecture and Helpers for the reusable pieces.