Commissions
Commissions
A pure, stateless commission model prices one fill at a time. The same model instance is safe to share across the simulator, the live fill decorator and the backtester — identical inputs always yield identical outputs.
On this page
What it is & when it runs
A commission model turns one fill into a charge. The built-in simulator calls it as it generates fills; the
live fill decorator calls it to enrich a broker fill that arrived with no fee. Whether a fill needs
enrichment at all is gated by the provider's ReportsCommission flag — a venue that
reports its own fees (crypto, or a sim engine charging its configured commission) is left untouched, so a
fee is never double-charged. The futures brokers report 0 and the platform computes it.
The model is pure and stateless. Per-order floor and cap (which span partial fills) are handled without the model holding state: the caller threads the running raw total back in on the next fill.
The ICommissionModel seam
ICommissionModel.csusing TradeStrike.Pipeline.Trading.Commissions;
public interface ICommissionModel
{
// Price one fill. Identical inputs always yield identical outputs.
CommissionResult Compute(in CommissionContext ctx);
}
Context & result
CommissionContext carries the per-fill inputs. It is deliberately asset-class-free —
asset class is a Desktop concept that cannot leak into this contracts assembly — so the Desktop
template model resolves symbol → rate itself and hands the math a plain context.
CommissionContext.csusing TradeStrike.Pipeline.Trading;
using TradeStrike.Pipeline.Trading.Commissions;
public readonly record struct CommissionContext(
string Symbol,
decimal Quantity, // THIS fill's quantity (positive magnitude)
decimal Price,
decimal Multiplier, // notional = Price × Quantity × Multiplier
OrderSide Side,
Liquidity Liquidity, // Maker / Taker / Unknown (selects maker/taker rate)
string Venue,
decimal PriorOrderRawCommission = 0m, // running pre-clamp raw on this order before this fill (0 on the first)
string? InstrumentKey = null); // canonical id for exact asset-class classification
public readonly record struct CommissionResult(
decimal Amount, // commission to stamp on the fill (a cost > 0, a rebate < 0)
decimal CumulativeRawCommission) // running pre-clamp raw INCLUDING this fill — thread back on the next
{
public static readonly CommissionResult Zero; // zero charge, zero accumulator
}
CumulativeRawCommission per order and pass it back as
the next fill's PriorOrderRawCommission. That is what lets a per-order minimum be paid exactly
once across partial fills and a per-order cap never be exceeded — while the model stays pure.
CommissionRate & the basis
A CommissionRate is the atomic rule: one basis, its value, plus optional per-order floor / cap
and crypto maker/taker overrides. A commission template stores one of these per asset-class or per-symbol.
CommissionRate.cs & enumsusing TradeStrike.Pipeline.Trading.Commissions;
public enum CommissionBasis
{
PerUnit, // Value per filled unit (futures: per contract; equities: per share)
PerOrder, // flat Value once per order, regardless of quantity (ticket fee)
PercentNotional, // Value percent of notional (0.1 = 0.1%)
BasisPoints, // Value basis points of notional (5 = 0.05%)
}
public enum CommissionAccrual
{
PerSide, // charged on every side/fill (NinjaTrader model). Round trip = 2 × Value
PerRound, // Value is the round-turn cost; each side pays half (Sierra-Chart model)
}
public enum Liquidity { Unknown, Maker, Taker } // simulator always supplies Unknown → uses plain Value
public sealed record CommissionRate
{
public static readonly CommissionRate Zero; // PerUnit, 0
public CommissionRate(
CommissionBasis basis,
decimal value, // may be negative to express a maker rebate
decimal minFee = 0m, // per-order floor on positive cost only. 0 = none
decimal maxFee = 0m, // per-order cap. 0 = uncapped
decimal? makerValue = null, // overrides Value for a maker fill
decimal? takerValue = null, // overrides Value for a taker fill
CommissionAccrual accrual = CommissionAccrual.PerSide);
public CommissionBasis Basis { get; init; }
public decimal Value { get; init; }
public decimal MinFee { get; init; } // validated non-negative
public decimal MaxFee { get; init; } // validated non-negative
public decimal? MakerValue { get; init; }
public decimal? TakerValue { get; init; }
public CommissionAccrual Accrual { get; init; }
}
Built-in models
RateCommissionModel is the one model that holds the math; every convenience wrapper delegates
to it. CommissionModels provides factories for the trivial cases so call sites never construct
a bare rate.
CommissionModels.csusing TradeStrike.Pipeline.Trading.Commissions;
// Shared zero-cost model — the default when no commission is configured.
ICommissionModel zero = CommissionModels.Zero;
// Flat per-unit, per-side model (the back-compat equivalent of a "per contract" charge).
ICommissionModel flat = CommissionModels.Flat(perUnit: 2.10m); // returns Zero when perUnit == 0
// A futures per-contract rate with a $0.50 per-order minimum, applied directly.
var rate = new CommissionRate(CommissionBasis.PerUnit, value: 0.85m, minFee: 0.50m);
ICommissionModel model = new RateCommissionModel(rate);
// A crypto percent-of-notional rate with maker rebate / taker fee.
var crypto = new CommissionRate(
CommissionBasis.PercentNotional, value: 0.10m,
makerValue: -0.02m, // maker rebate
takerValue: 0.05m); // taker fee
ICommissionModel cryptoModel = new RateCommissionModel(crypto);
How the math works
RateCommissionModel.Compute is the single source of truth for basis, maker/taker, accrual and
the per-order floor/cap. The floor and cap apply to the whole order, not each partial fill —
the model threads the running raw total so a minimum is paid exactly once and a cap is never exceeded:
RateCommissionModel.cs (essence)// This fill's charge = clamp(priorRaw + rawThisFill) − clamp(priorRaw)
decimal cumulativeRaw = priorRaw + RawContribution(rate, ctx);
decimal amount = Clamp(rate, cumulativeRaw) - Clamp(rate, priorRaw);
return new CommissionResult(amount, cumulativeRaw);
-
Basis —
PerUnitchargesValue × Quantity;PercentNotionalandBasisPointscharge againstPrice × Quantity × Multiplier;PerOrdercontributes the flat fee only on the first fill (when nothing has accrued yet). -
Accrual — a
PerRoundrate is applied as a 0.5× per-side factor so a round trip sums to the full round-turn cost. -
Maker/Taker —
MakerValue/TakerValueoverrideValuewhen the fill'sLiquiditymatches; the simulator suppliesUnknownand falls back toValue. -
Rebates — a negative
Valueyields a negative charge; the floor is applied to positive cost only, so a rebate is never forced positive.
pricing partial fillsvar model = new RateCommissionModel(new CommissionRate(CommissionBasis.PerUnit, 0.85m, minFee: 1.00m));
// First partial: 1 contract. Raw = 0.85, floored up to the 1.00 minimum.
var f1 = model.Compute(new CommissionContext("ES", 1m, 5000m, 50m, OrderSide.Buy, Liquidity.Unknown, "sim"));
// f1.Amount == 1.00, f1.CumulativeRawCommission == 0.85
// Second partial: 1 more. Thread the accumulator back in.
var f2 = model.Compute(new CommissionContext("ES", 1m, 5000m, 50m, OrderSide.Buy, Liquidity.Unknown, "sim",
PriorOrderRawCommission: f1.CumulativeRawCommission));
// raw now 1.70 (> minimum) → f2.Amount == 0.70, so the order paid 1.70 total — the minimum charged once.
Writing a custom model
Most needs are met by configuring a CommissionRate and wrapping it in
RateCommissionModel. Implement ICommissionModel directly only when you must
select a rate per fill — the canonical example is a per-symbol / per-asset-class lookup that
resolves a rate then delegates to RateCommissionModel (do not re-implement the math). Keep your
Compute pure: identical inputs, identical output.
PerVenueCommissionModel.csusing System.Collections.Generic;
using TradeStrike.Pipeline.Trading.Commissions;
// Resolves a per-venue rate, then delegates to the one model that holds the math.
public sealed class PerVenueCommissionModel : ICommissionModel
{
private readonly IReadOnlyDictionary<string, RateCommissionModel> _byVenue;
private readonly RateCommissionModel _fallback;
public PerVenueCommissionModel(
IReadOnlyDictionary<string, CommissionRate> ratesByVenue, CommissionRate fallback)
{
var map = new Dictionary<string, RateCommissionModel>();
foreach (var kv in ratesByVenue) map[kv.Key] = new RateCommissionModel(kv.Value);
_byVenue = map;
_fallback = new RateCommissionModel(fallback);
}
public CommissionResult Compute(in CommissionContext ctx)
{
var model = _byVenue.TryGetValue(ctx.Venue, out var m) ? m : _fallback;
return model.Compute(in ctx); // single source of truth for the math
}
}
CumulativeRawCommission the caller threads back — a stateful model would break the
"shared across sim, live and backtest" guarantee and produce non-deterministic backtests.