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 price a broker fill that arrived without the venue's fee. A fill that
carries the venue's own fee is a broker fact and is left as it is, so a fee is never charged twice. The
futures brokers report 0 on their fills and the platform prices them.
For every live fill the decorator takes the first of these that applies:
- The fill carries the venue's own fee:
Fill.CommissionIsAuthoritativeis true, or the provider'sReportsCommissionis true. The fill passes through untouched. - The venue publishes a rate for this account and product (
IVenueCommissionRateSource) and the trader has broker rates switched on in the commission settings (the default). That rate prices it. - The trader's commission settings price it.
- Nothing is configured: zero.
The model chosen for an order's first priced fill is kept for the rest of that order, so partial fills of one order never mix two rate tables (and two different floors or caps). The next order picks up whatever changed.
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.
Venue-reported fees & published rates
If you are writing a trading provider, four members decide how your fees reach the trader. Get them right and the blotter, the journal and the PnL figures show the broker's real charge; get them wrong and a fee is either missing or charged twice.
| Member | What you set |
|---|---|
Fill.CommissionIsAuthoritative |
true on each fill whose Commission is the venue's own fee for that execution,
a genuine 0 on a commission-free venue included. Leave it false, with Commission = 0,
when the venue reported nothing usable: the platform then prices the fill. It is per fill because several
venues report fees on some paths only (fees on history but not on live fills, or on closing fills only). |
VenueOrderReport.LastFillCommissionIsAuthoritative |
The same fact when you fold order reports through OrderUpdateFold: set it with
LastFillCommission and the fold carries it onto the Fill it emits. |
ITradingProvider.ReportsCommission |
A coarse shortcut: true only when every fill of your provider carries the venue's
fee (crypto venues that report the exchange fee, a simulator charging its own commission). Default
false. Stamping each fill is the mechanism; this flag only saves the per-fill check. |
ITradingProvider.RealizedPnLIncludesCommissions |
Whether your Account.RealizedPnL has fees deducted. true: it is net (the
built-in simulator charges its fee into it). false: it is gross price PnL, and a consumer may
show an estimated net by subtracting the session's recorded commissions, labelled as an estimate.
null (the default): undeclared, and consumers derive and show no net figure at all. Declare it
only once you have proven it on the wire: a wrongly derived net misstates a prop trader's distance to the
daily-loss limit. |
stamping venue feesusing SabrTrader.Pipeline.Venues.Trading;
// One execution from the venue. exec.Fee is null when the venue sent no fee for it.
var fill = new Fill(orderId, exec.Quantity, exec.Price, exec.Fee ?? 0m, exec.TimestampUtc)
{
ExecutionId = exec.Id,
CommissionIsAuthoritative = exec.Fee is not null,
};
// The same fact through the fold: the flag travels onto the Fill the fold emits.
var report = new VenueOrderReport(
State: OrderState.Working,
FilledQuantity: exec.CumulativeQuantity,
TimestampUtc: exec.TimestampUtc,
AverageFillPrice: exec.AveragePrice,
LastFillQuantity: exec.Quantity,
LastFillPrice: exec.Price,
LastFillCommission: exec.Fee ?? 0m,
LastFillCommissionIsAuthoritative: exec.Fee is not null);
Some venues publish the rates they will charge rather than a fee per fill: Rithmic's FCM-configured RMS
commission rates are the model case. Those rates are per account (two prop firms can charge different
rates for the same product on the same day), which one global settings table cannot express. Implement
the optional IVenueCommissionRateSource facet on your provider and the decorator prices your
unstamped fills from it, ahead of the trader's own settings. The decorator derives the product root from
the order's instrument (a dated MNQZ5 asks for MNQ) and probes the facet with
AsCapability<IVenueCommissionRateSource>(), so it is found through any decorator around
your provider. VenueCommissionRates feeds the read-only list in the commission settings that
shows which table priced a fill.
IVenueCommissionRateSource.csusing System.Collections.Generic;
using SabrTrader.Pipeline.Venues.Trading;
using SabrTrader.Pipeline.Venues.Trading.Commissions;
public interface IVenueCommissionRateSource
{
// The venue's rate for one product root on one account; null when it published none
// (the trader's commission settings then price the fill).
CommissionRate? TryGetVenueCommissionRate(AccountId account, string productRoot);
// Every rate published for the account, for display. Empty when nothing is published.
IReadOnlyList<VenueCommissionRate> VenueCommissionRates(AccountId account);
}
public readonly record struct VenueCommissionRate(string ProductRoot, CommissionRate Rate);
publishing venue ratesusing System;
using System.Collections.Generic;
using System.Linq;
using SabrTrader.Pipeline.Venues.Trading;
using SabrTrader.Pipeline.Venues.Trading.Commissions;
public sealed partial class MyBrokerTradingProvider : IVenueCommissionRateSource
{
// Swapped whole when the venue sends a new table, so readers never see a half-built one.
private volatile IReadOnlyDictionary<AccountId, IReadOnlyDictionary<string, CommissionRate>> _rates =
new Dictionary<AccountId, IReadOnlyDictionary<string, CommissionRate>>();
public CommissionRate? TryGetVenueCommissionRate(AccountId account, string productRoot) =>
_rates.TryGetValue(account, out var byRoot) && byRoot.TryGetValue(productRoot, out var rate)
? rate
: null;
public IReadOnlyList<VenueCommissionRate> VenueCommissionRates(AccountId account) =>
_rates.TryGetValue(account, out var byRoot)
? byRoot.Select(kv => new VenueCommissionRate(kv.Key, kv.Value)).ToArray()
: Array.Empty<VenueCommissionRate>();
// The venue's table for one account: a per-side, per-contract amount in account currency.
private void OnRateTable(AccountId account, IReadOnlyDictionary<string, decimal> perSideByRoot)
{
var table = perSideByRoot.ToDictionary(
kv => kv.Key,
kv => new CommissionRate(CommissionBasis.PerUnit, kv.Value));
var next = new Dictionary<AccountId, IReadOnlyDictionary<string, CommissionRate>>(_rates)
{
[account] = table,
};
_rates = next;
}
}
null for a root your venue published no rate for; the trader's settings take over.
A venue that publishes no rates at all does not implement the facet.
Trade history follows the same rule. PersistedFill.CommissionIsAuthoritative is carried from
the fill: when the live capture and a history backfill of the same execution meet on one fill key, a fee
marked authoritative replaces the stored one and an estimate never overwrites anything, so the broker's
real charge wins whichever path landed first. Set it on the fills your
history source reports when they carry the venue's fee.
The ICommissionModel seam
ICommissionModel.csusing SabrTrader.Pipeline.Venues.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 carries no rate and no asset class:
the caller resolves which CommissionRate applies (the platform's commission templates look it
up by symbol, asset class or venue) and hands the math a plain context. InstrumentKey, when
set, is the canonical id a resolving model can classify the instrument by.
CommissionContext.csusing SabrTrader.Pipeline.Venues.Trading;
using SabrTrader.Pipeline.Venues.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 SabrTrader.Pipeline.Venues.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 SabrTrader.Pipeline.Venues.Trading.Commissions;
// Shared zero-cost model — the default when no commission is configured.
ICommissionModel zero = CommissionModels.Zero;
// Flat per-unit, per-side model: a plain "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 SabrTrader.Pipeline.Venues.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.