signed_notional
to
signed_cost_basis
and
exposing
the
contract
multiplierDate: 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)
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:
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.
signed_notional = 1033.74;
the
real
notional
is
5168.7
USD.
1033.74
is
not
a
dollar
amount
at
all.
unrealized_pnl,
realized_pnl,
and
every
initial_margin_*
are
USD,
while
signed_notional
and
average_price
are
in
contract-price
units.
unrealized_pnl = (qty * mark − signed_notional) * multiplier
only
type-checks
dimensionally
because
signed_notional
is
multiplier-free.
The
current
behaviour
is
load-bearing
and
deliberate
—
the
defect
is
that
the
name
advertises
the
opposite.
The value itself (a pre-multiplier cost basis) is useful and downstream-critical; only the name and the missing multiplier are the problem.
The thread converged, with Ben Xie and Tin Chung concurring, on:
signed_notional
→
signed_cost_basis
on
the
SDK
types
(Position,
SymbolRiskSnapshot).
The
new
field
carries
the
identical
value;
only
the
name
changes.
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."
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:
signed_cost_basis_notional
(Ben's
suggestion,
since
"notional
is
usually
post-multiplier").
Dropped
because
once
we
ship
multiplier
alongside
signed_cost_basis,
the
notional
is
trivially
derivable
and
a
third
name
is
redundant.
signed_notional.
rename-signed-notionalrs/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
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."*
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.
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.
signed_notional
keeps
returning.
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.
Position
exposes
signed_cost_basis
but
not
multiplier.
As
implemented,
only
SymbolRiskSnapshot
carries
multiplier;
Position
does
not.
A
holder
of
a
bare
Position
therefore
still
cannot
compute
notional
—
arguably
reintroducing
the
same
gap
for
that
type.
PR
#3242's
description
says
the
multiplier
was
added
to
"Position/SymbolRiskSnapshot,"
so
this
asymmetry
may
be
an
oversight
rather
than
a
decision.
Needs
a
call
before
merge:
add
multiplier
to
Position
too,
or
document
why
it's
snapshot-only.
signed_notional.
"A
future
release"
is
unspecified.
No
deprecation
window,
target
version,
or
consumer-comms
plan
has
been
agreed.
RiskPosition,
RedisSymbolRiskSnapshot,
and
ChSymbolRiskSnapshot
still
say
signed_notional
while
the
SDK
says
signed_cost_basis.
Deliberate
(scope
control),
but
it
means
the
misleading
name
persists
internally
and
the
two
names
must
be
kept
mentally
aliased
by
future
maintainers.
Joe
flagged
wanting
to
migrate/alias
the
DB
columns
and
internal
structs
but
judged
it
"harder
to
justify."
signed_notional
even
useful
to
end
users?
Tin
asked
this
directly.
Consensus
kept
the
value
(as
signed_cost_basis)
because
the
cost
basis
is
useful;
the
"notional
cost
basis"
reading
(post-multiplier)
was
judged
only
debatably
useful
and
possibly
not
a
standard
term.
Not
fully
resolved
—
revisit
if
we
ever
drop
the
field
entirely
rather
than
rename
it.
multiplier
is
None
for
risk
snapshots
written
before
#3256,
so
notional
cannot
be
reconstructed
for
those
rows.
Acceptable
given
multipliers
don't
change
over
an
instrument's
life
(Andrew
Lee),
but
any
backfill
of
multiplier
onto
old
rows
is
out
of
scope
here.
hold merge
and
in
merge-conflict
(dirty).
It
needs
a
rebase
and
the
hold
cleared
before
it
can
land.
Joe McCarey (author, A-4258), Tin Chung (implementation, #3242/#3265), Ben Xie (terminology), Michael Rees, Andrew Lee (multiplier-lifetime note).