Accounts & balances
Accounts & balances
What a provider must publish for an account: a core financial snapshot, an optional extended metrics record, and — for multi-asset venues — per-wallet balances.
On this page
AccountId & mode
An account is identified by a composite AccountId with three axes — the provider that
hosts it, the provider's own opaque account identifier, and the operational mode. All three participate in
equality and hash, so the SAME provider account-id used across modes never aliases.
AccountId.csusing TradeStrike.Pipeline.Trading;
public readonly record struct AccountId(string ProviderKey, string ProviderAccountId, AccountMode Mode)
{
public static readonly AccountId None; // sentinel "no account"
public bool IsEmpty { get; }
public override string ToString(); // "providerKey/accountId/mode"
public static bool TryParse(string? text, out AccountId id);
}
public enum AccountMode
{
Live, // real broker, real money — UI badges red, adds a confirm step over a threshold
Paper, // broker-side sandbox (same adapter as Live) — UI badge amber
Sim, // local in-process simulation, persisted on disk — UI badge blue
}
ToString renders the stable "providerKey/accountId/mode" form used in log lines
and workspace references; TryParse round-trips it so a selected account survives restart.
The mode is load-bearing — routing sends Sim accounts to the built-in simulator and
Live/Paper to the broker adapter, and the UI colour-codes for visual safety.
The Account snapshot
Account is an immutable record. Providers emit a fresh instance on every meaningful change
(deposit, realized-PnL crystallization, unrealized mark-to-market) via an AccountSnapshotEvent;
the UI binds to whichever instance is currently in the service's cache. Every money axis is
decimal — float drift on cumulative PnL is unacceptable.
Account.csusing TradeStrike.Pipeline.Trading;
public sealed record Account(
AccountId Id,
string DisplayName,
string Currency,
decimal CashValue,
decimal RealizedPnL,
decimal UnrealizedPnL,
decimal MarginUsed,
decimal BuyingPower,
DateTime UpdatedUtc)
{
public bool IsSnapshot { get; init; } // true when projected from a broker SNAPSHOT frame
public AccountMetrics? Metrics { get; init; } // optional extended metrics (null when not supplied)
public AccountMode Mode { get; } // read-through from Id
public decimal Equity { get; } // CashValue + UnrealizedPnL
public static Account NewSim(AccountId id, string displayName, string currency, decimal startingCash);
}
Equity (= CashValue + UnrealizedPnL) and Mode are computed
conveniences. IsSnapshot distinguishes a broker-side snapshot frame (the first PnL frame after
subscribe, or a fresh one after reconnect) from a live delta — useful for a "resyncing…" indicator.
The NewSim factory gives a freshly-created sim account: zero PnL, no margin, full buying power
equal to starting cash.
Extended metrics
The base Account stays minimal — the figures every venue supplies and the UI binds
hottest. Richer figures a venue may publish (margin decomposition, prop-firm risk, activity
counts, options exposure) ride alongside in an optional AccountMetrics record, surfacing the
NinjaTrader-parity Accounts-grid columns without coupling the model to any one broker.
Account fields, so a stored
copy can never drift from the authoritative source.
AccountMetrics.csusing TradeStrike.Pipeline.Trading;
public sealed record AccountMetrics
{
// Cash / balances
public decimal? TotalCashBalance { get; init; }
public decimal? GrossRealizedPnL { get; init; } // gross closed PnL BEFORE commissions
public decimal? TotalCommissions { get; init; } // cumulative commission today
// Margin / buying-power decomposition
public decimal? MaintenanceMargin { get; init; }
public decimal? ExcessBuyMargin { get; init; }
public decimal? ExcessSellMargin { get; init; }
public decimal? UsedBuyingPower { get; init; }
public decimal? ReservedBuyingPower { get; init; }
// Risk / prop-firm eval
public decimal? DailyLossLimit { get; init; }
public decimal? DrawdownPercentUsed { get; init; } // 0–100
public decimal? TrailingDrawdownFloor { get; init; }
public string? AutoLiquidate { get; init; } // broker passthrough ("yes"/"no"/empty)
// Activity counts
public int? NetQuantity { get; init; }
public int? OpenPositions { get; init; }
public int? FilledBuys { get; init; }
public int? FilledSells { get; init; }
public int? WorkingBuys { get; init; }
public int? WorkingSells { get; init; }
// Options exposure
public decimal? LongOptionValue { get; init; }
public decimal? ShortOptionValue { get; init; }
}
publishing metricsvar acct = baseAccount with
{
Metrics = new AccountMetrics
{
MaintenanceMargin = 1_320m,
UsedBuyingPower = 6_600m,
NetQuantity = -2,
OpenPositions = 1,
DailyLossLimit = 1_000m,
DrawdownPercentUsed = 42.5m,
}
};
Emit(new AccountSnapshotEvent(DateTime.UtcNow, acct));
Because AccountMetrics is an immutable, value-equal record, an Account whose only
change is a metric flip is != the previous one — exactly what the row view-model's diff
relies on to repaint.
Per-asset balances
A futures account holds one cash figure (Account.CashValue). A crypto-spot account holds many
wallets — BTC, ETH, USDT — each an AccountBalance. Providers with multi-asset
wallets declare the AssetBalances capability and emit a BalanceSnapshotEvent
carrying the full set of non-zero balances (snapshot semantics, so a consumer rebinds without merging
deltas). Futures providers simply never declare it.
AccountBalance.csusing TradeStrike.Pipeline.Trading;
public sealed record AccountBalance
{
public string Asset { get; } // upper-cased, e.g. "BTC", "USDT"
public decimal Total { get; } // available + reserved
public decimal Available { get; } // free — spendable now
public decimal Reserved { get; } // derived: Total − Available
// asset is trimmed + upper-cased; throws on blank asset, negative figures,
// or available > total (reserved can never be negative).
public static AccountBalance Create(string asset, decimal total, decimal available);
}
emitting balancesvar balances = new[]
{
AccountBalance.Create("BTC", total: 0.75m, available: 0.50m), // 0.25 reserved in orders
AccountBalance.Create("USDT", total: 5_000m, available: 5_000m),
};
Emit(new BalanceSnapshotEvent(DateTime.UtcNow, accountId, balances));
What a provider must publish
- On
StartAsync, expose discovered accounts via theAccountssnapshot and emit anAccountSnapshotEventper account (declareAccountDiscovery). - On every financial change, emit a fresh
Accountinstance. SetIsSnapshoton a broker snapshot frame; leave it false for live deltas. - Populate
Metricswith whatever your venue reports; leave unknown fields null. - For multi-asset venues, declare
AssetBalancesand emitBalanceSnapshotEventwith the full non-zero set whenever a wallet changes. - On a closed/removed account, emit
AccountRemovedEvent— no further events for that id will follow.