RFC: Renaming signed_notional to signed_cost_basis and exposing the contract multiplier

Date: 2026-07-24

Status: Proposed SDK change is In Review (#3242, hold merge); GUI companion open (#3265); ClickHouse multiplier persistence merged (#3256). This RFC documents the consensus reached in the #ax-devs-requests thread and the shape already implemented on rename-signed-notional.

Related: A-4258 (this work), A-3952 ("Margin open notional is pre-multiplier" the bug this trap already shipped), A-4307 (persist contract multiplier in the risk-snapshot ClickHouse schema)

Background

Position.signed_notional and SymbolRiskSnapshot.signed_notional both on the published SDK (ax-exchange-sdk) and in docs/public/openapi/api-gateway.json are not notional values. Each is a cost basis accumulated from fill prices (Σ fill price * quantity, rs/btnl-trade-engine/src/positions.rs:91,96,105) with no contract multiplier applied.

Two things are wrong with the name, on independent axes:

  1. No multiplier, so it is not a USD figure unlike every neighbouring field on the same payload.
  2. Accumulated from fill prices, not the mark, so even multiplier-adjusted it is a cost basis at entry, not exposure at mark. average_price is literally derived from it as signed_notional / signed_quantity (rs/sdk-internal/risk/src/buying_power_updater.rs:108).

Notional is neither of those. The codebase even defines the term correctly one crate away: rs/sdk-internal/src/notional.rs documents notional(price, quantity, multiplier) = price * quantity * multiplier as "the single canonical notional computation," and its test asserts a signed notional includes the multiplier. So signed notional names two contradictory things in the same repo, and the field is exactly the price * quantity the canonical helper says must never bypass it.

Why this is worth more than a doc comment

The value itself (a pre-multiplier cost basis) is useful and downstream-critical; only the name and the missing multiplier are the problem.

Goals

Non-goals

Decisions (consensus from the Slack thread)

The thread converged, with Ben Xie and Tin Chung concurring, on:

  1. Rename signed_notional signed_cost_basis on the SDK types (Position, SymbolRiskSnapshot). The new field carries the identical value; only the name changes.
  2. Expose the bare multiplier rather than a pre-computed notional. Rationale: with signed_cost_basis + multiplier, any consumer who wants notional cost basis can compute it, and we avoid inventing a semi-standard field. Tin: "can provide notional_usd or multiplier but multiplier is more flexible." Joe: "just provide the bare components that everything can be calculated from."
  3. Keep signed_notional as a deprecated alias returning the same value, so nothing breaks. Marked #[deprecated(note = "use signed_cost_basis")]; "will be removed in a future release."

Naming alternatives considered and dropped:

Design as implemented on rename-signed-notional

Public SDK (rs/sdk/src/protocol/api_gateway.rs)

Position gains:

/// Signed cost basis (Σ fill `price * quantity`) — excludes the contract
/// multiplier, so it is not a USD notional.
pub signed_cost_basis: Decimal,
/// **Deprecated:** renamed to `signed_cost_basis` (same value)...
#[deprecated(note = "use `signed_cost_basis`")]
pub signed_notional: Decimal,

SymbolRiskSnapshot gains signed_cost_basis, the deprecated signed_notional alias, and:

/// Contract multiplier for the symbol — the bare component to derive
/// notional from `signed_quantity` and a price. `None` when unknown.
#[serde(default)]
pub multiplier: Option<Decimal>,

multiplier is Option<Decimal> with #[serde(default)] so payloads written by older binaries (which omit the key) deserialize to None rather than erroring during a rolling deploy. Same treatment is mirrored on the internal carriers RedisSymbolRiskSnapshot (redis_values.rs) and ChSymbolRiskSnapshot (clickhouse/schema.rs), each with a round-trip / defaults-to-None test.

Internal plumbing (unchanged names, aliased at the boundary)

Internal structs keep the signed_notional name; the SDK conversion sets both SDK fields from the one internal value:

// e.g. From<RedisSymbolRiskSnapshot> / From<ChSymbolRiskSnapshot> for SymbolRiskSnapshot
signed_cost_basis: v.signed_notional,
signed_notional:   v.signed_notional,   // #[allow(deprecated)] on the fn
multiplier:        v.multiplier,

fuse_risk_snapshot (api-gateway/src/utils.rs) threads signed_cost_basis and multiplier from the risk-engine row through to the fused snapshot, with a unit test that the multiplier survives when order-gateway has no margin snapshot. Internal RiskPosition.signed_notional keeps its name but its doc comment is corrected to "Signed cost basis (Σ fill pricequantity), no multiplier not notional."*

Multiplier persistence (A-4307, merged as #3256)

For the multiplier to be non-None on historical/ClickHouse-sourced snapshots it has to be written at snapshot time. #3256 persists the contract multiplier into the risk-snapshot rows (ChSymbolRiskSnapshot.multiplier, populated from buying_power_updater). Rows written before that land as None.

GUI (#3265)

Every positions/margin/risk consumer and column id is switched to signed_cost_basis; without it "Open Notional renders NaN." The GUI also reads the optional multiplier. #3265 must deploy together with #3242.

Rollout

  1. Land the SDK/api-gateway change (#3242) additive, non-breaking; both field names go out on the wire.
  2. Land the GUI change (#3265) in the same deploy so internal consumers move to the new name and the multiplier.
  3. External consumers migrate at their own pace while signed_notional keeps returning.
  4. Remove signed_notional in a later, explicitly-versioned breaking release (timeline TBD).

Because both fields ship simultaneously and old payloads default multiplier to None, mixed-version producers/consumers are safe across a rolling deploy the disconnect/restart/reconnect matrix here is just "old binary omits multiplier," covered by the defaults-to-None tests.

Deferred / open questions

Appendix participants

Joe McCarey (author, A-4258), Tin Chung (implementation, #3242/#3265), Ben Xie (terminology), Michael Rees, Andrew Lee (multiplier-lifetime note).