use crate::core::account_outcome::{AccountAdjustmentOutcome, OutcomeAmount, PnlOutcomeAmount};
use rust_decimal::Decimal;
use crate::core::sync_mode::SyncMode;
use crate::core::{
AccountOutcomeEntry, HasAccountId, HasExecutionReportFillFee, HasExecutionReportIsFinal,
HasExecutionReportLastTrade, HasInstrument, HasLeavesQuantity, HasPreTradeLock, HasSide,
Instrument,
};
use crate::marketdata::{MarketDataError, MarketDataSync, Quote, QuoteResolution};
use crate::param::{
AccountId, Asset, MonetaryAmount, Pnl, PositionSize, Price, Quantity, Side, Trade,
};
use crate::pretrade::holdings::{AdjustmentOverflowError, Holdings};
use crate::pretrade::policy::{missing_required_field_account_block, PolicyGroupId};
use crate::pretrade::{AccountBlock, PostTradeContext, PostTradeResult, PreTradeLock, RejectCode};
use crate::storage::ConfigCell;
use super::rejects::arithmetic_overflow_account_block;
use super::views::{ExecutionRequestView, FillCancelDeltas, LegDelta, LegKind};
use super::{HoldingsKey, SpotFundsPolicy};
use crate::pretrade::policies::pnl_bounds;
impl<Sync, MarketDataSyncMode> SpotFundsPolicy<Sync, MarketDataSyncMode>
where
Sync: SyncMode,
Sync::StorageLockingPolicyFactory: crate::storage::LockingPolicyFactory,
MarketDataSyncMode: MarketDataSync,
{
pub(super) fn mutate_slot<F>(
&self,
key: HoldingsKey,
mutation: F,
) -> Result<Holdings, AdjustmentOverflowError>
where
F: FnOnce(Holdings) -> Result<Holdings, AdjustmentOverflowError>,
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let key_for_remove = key.clone();
let (result, was_new) = self.holdings.with_mut_or_insert_prune_new_if_zero(
key,
Holdings::zero,
|slot, is_new| {
let new = mutation(*slot)?;
*slot = new;
Ok((new, is_new))
},
)?;
if result.is_zero() && !was_new {
self.holdings.remove_if_zero(&key_for_remove);
}
Ok(result)
}
fn accounting_quote(
&self,
account_id: AccountId,
ctx: &PostTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
instrument: &Instrument,
) -> Option<Quote>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let market_orders = self.market_orders.as_ref()?;
let instrument_id = market_orders.resolve(instrument)?;
match market_orders.market_data.get(
instrument_id,
account_id,
ctx,
QuoteResolution::AccountThenGroupThenDefault,
) {
Ok(quote) | Err(MarketDataError::QuoteExpired(quote)) => Some(quote),
Err(MarketDataError::QuoteUnavailable | MarketDataError::UnknownInstrument) => None,
}
}
fn account_currency_factor(
&self,
account_id: AccountId,
ctx: &PostTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
source_asset: &Asset,
account_currency: &Asset,
) -> Result<Option<Decimal>, AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
if source_asset == account_currency {
return Ok(Some(Decimal::ONE));
}
let direct = Instrument::new(source_asset.clone(), account_currency.clone());
if let Some(mark) = self
.accounting_quote(account_id, ctx, &direct)
.and_then(|quote| quote.mark)
{
return Ok(Some(mark.to_decimal()));
}
let inverse = Instrument::new(account_currency.clone(), source_asset.clone());
if let Some(mark) = self
.accounting_quote(account_id, ctx, &inverse)
.and_then(|quote| quote.mark)
{
let Some(factor) = Decimal::ONE.checked_div(mark.to_decimal()) else {
return Ok(None);
};
return Ok(Some(factor));
}
Ok(None)
}
fn account_currency_price(
&self,
account_id: AccountId,
ctx: &PostTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
quote_asset: &Asset,
account_currency: &Asset,
trade_price: Price,
) -> Result<Option<Price>, AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let Some(factor) =
self.account_currency_factor(account_id, ctx, quote_asset, account_currency)?
else {
return Ok(None);
};
trade_price_with_factor(Self::NAME, trade_price, factor).map(Some)
}
fn pnl_barrier_for(
&self,
account_id: AccountId,
account_group_id: Option<crate::param::AccountGroupId>,
account_currency: &Asset,
) -> Option<super::SpotFundsPnlBoundsBarrier> {
self.settings.with(|settings| {
settings
.pnl_barrier_for(account_id, account_group_id, account_currency)
.cloned()
})
}
fn pnl_missing_fx_block(
&self,
account_id: AccountId,
source_asset: &Asset,
account_currency: &Asset,
) -> AccountBlock {
pnl_bounds::pnl_calculation_failed_block(
self,
"pnl calculation failed",
format!(
"pnl calculation failed: PnL could not be computed due to \
missing FX for account {account_id}, source asset \
{source_asset}, account currency {account_currency}"
),
)
}
fn pnl_arithmetic_failed_block(&self, details: String) -> AccountBlock {
pnl_bounds::pnl_calculation_failed_block(self, "pnl calculation failed", details)
}
fn fee_pnl_delta(
&self,
account_id: AccountId,
ctx: &PostTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
fee: &MonetaryAmount,
account_currency: &Asset,
) -> Result<Option<Pnl>, AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let Some(factor) =
self.account_currency_factor(account_id, ctx, &fee.currency, account_currency)?
else {
return Ok(None);
};
let Some(value) = fee.amount.to_pnl().to_decimal().checked_mul(factor) else {
return Err(self.pnl_arithmetic_failed_block(format!(
"fee pnl conversion overflow: account {account_id}, fee {} {}, \
account currency {account_currency}",
fee.amount, fee.currency
)));
};
Ok(Some(Pnl::new(value)))
}
#[allow(clippy::too_many_arguments)]
fn apply_fee_debit(
&self,
account_id: AccountId,
underlying_asset: &Asset,
settlement_asset: &Asset,
fee: &MonetaryAmount,
deltas: &mut FillCancelDeltas,
) -> Result<(), AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let amount = fee.amount.to_position_size();
if amount.is_zero() {
return Ok(());
}
let new_h = self
.mutate_slot((account_id, fee.currency.clone()), |h| {
h.apply_fill_inflow(amount)
})
.map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fee debit overflow: account {account_id}, currency {}, \
fee {}",
fee.currency, fee.amount
),
)
})?;
let leg = deltas.fee_leg_mut(&fee.currency, underlying_asset, settlement_asset);
leg.balance_delta = leg.balance_delta.checked_add(amount).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fee balance delta overflow: account {account_id}, \
currency {}, fee {}",
fee.currency, fee.amount
),
)
})?;
leg.final_holdings = Some(new_h);
Ok(())
}
fn apply_account_pnl_delta(
&self,
account_id: AccountId,
account_currency: &Asset,
barrier: &super::SpotFundsPnlBoundsBarrier,
delta: Pnl,
) -> Option<AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
self.pnl.with_mut(
(account_id, account_currency.clone()),
|| Pnl::ZERO,
|entry, _is_new| {
let previous = *entry;
let updated = match previous.checked_add(delta) {
Ok(updated) => updated,
Err(_) => {
return Some(self.pnl_arithmetic_failed_block(format!(
"spot-funds account pnl overflow: previous {previous}, \
delta {delta}, account currency {account_currency}, \
account {account_id}"
)));
}
};
*entry = updated;
let sides =
pnl_bounds::breached_sides(barrier.lower_bound, barrier.upper_bound, updated);
if sides.is_empty() {
None
} else {
Some(pnl_bounds::pnl_breach_account_block(
Self::NAME,
format!(
"{} bound breached: realized pnl {updated}, \
lower_bound {:?}, upper_bound {:?}, \
account currency {account_currency}, account {account_id}",
sides.join(" and "),
barrier.lower_bound,
barrier.upper_bound
),
))
}
},
)
}
pub(super) fn read_execution_request<'i, ExecutionReport>(
&self,
report: &'i ExecutionReport,
) -> Result<ExecutionRequestView<'i>, AccountBlock>
where
ExecutionReport: HasInstrument
+ HasAccountId
+ HasSide
+ HasExecutionReportLastTrade
+ HasExecutionReportFillFee
+ HasLeavesQuantity
+ HasExecutionReportIsFinal
+ HasPreTradeLock,
{
let account_id = report
.account_id()
.map_err(|e| missing_required_field_account_block(self, "account ID", &e))?;
let instrument = report
.instrument()
.map_err(|e| missing_required_field_account_block(self, "instrument", &e))?;
let side = report
.side()
.map_err(|e| missing_required_field_account_block(self, "side", &e))?;
let last_trade = report
.last_trade()
.map_err(|e| missing_required_field_account_block(self, "last fill", &e))?;
let fee = report
.fill_fee()
.map_err(|e| missing_required_field_account_block(self, "fill fee", &e))?;
let leaves_quantity = report
.leaves_quantity()
.map_err(|e| missing_required_field_account_block(self, "remaining quantity", &e))?;
let is_final = report
.is_final()
.map_err(|e| missing_required_field_account_block(self, "order finality", &e))?;
let lock = report
.lock()
.map_err(|e| missing_required_field_account_block(self, "pre-trade lock", &e))?;
Ok(ExecutionRequestView {
instrument,
account_id,
side,
last_trade,
fee,
leaves_quantity,
is_final,
lock,
})
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_trade_fill(
&self,
ctx: &PostTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
account_id: AccountId,
underlying_asset: &Asset,
settlement_asset: &Asset,
side: Side,
trade: Trade,
fee: Option<&MonetaryAmount>,
lock: &PreTradeLock,
deltas: &mut FillCancelDeltas,
) -> Result<(), AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let qty_pos = trade.quantity.to_position_size();
let settlement_notional = trade
.price
.calculate_position_size(trade.quantity)
.map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fill notional volume overflow: account {account_id}, \
asset {settlement_asset}, px {}, qty {}",
trade.price, trade.quantity,
),
)
})?;
let (underlying_consume, underlying_flow) = match side {
Side::Buy => (PositionSize::ZERO, qty_pos),
Side::Sell => (qty_pos, neg(qty_pos)),
};
let touches_position_accounting = !underlying_flow.is_zero();
let account_currency = ctx.account_currency();
let pnl_barrier = account_currency
.as_ref()
.and_then(|currency| self.pnl_barrier_for(account_id, ctx.account_group(), currency));
let account_currency_price = if touches_position_accounting {
match account_currency.as_ref() {
Some(account_currency) => match self.account_currency_price(
account_id,
ctx,
settlement_asset,
account_currency,
trade.price,
)? {
Some(price) => Some(price),
None if pnl_barrier.is_some() => {
return Err(self.pnl_missing_fx_block(
account_id,
settlement_asset,
account_currency,
));
}
None => None,
},
None => None,
}
} else {
None
};
let settlement_consume =
self.settlement_fill_consume(account_id, settlement_asset, side, trade, lock)?;
let settlement_flow = match side {
Side::Buy => neg(settlement_notional),
Side::Sell => settlement_notional,
};
let underlying_incoming_consume = match side {
Side::Buy => qty_pos,
Side::Sell => PositionSize::ZERO,
};
let settlement_incoming_consume =
self.settlement_incoming_amount(account_id, settlement_asset, side, trade, lock)?;
let fee_pnl_delta = if let (Some(account_currency), Some(_), Some(fee)) =
(account_currency.as_ref(), pnl_barrier.as_ref(), fee)
{
match self.fee_pnl_delta(account_id, ctx, fee, account_currency)? {
Some(delta) => Some(delta),
None => {
return Err(self.pnl_missing_fx_block(
account_id,
&fee.currency,
account_currency,
));
}
}
} else {
None
};
let underlying_leg = (
LegKind::Underlying,
underlying_asset,
underlying_consume,
underlying_flow,
underlying_incoming_consume,
account_currency_price,
);
let settlement_leg = (
LegKind::Settlement,
settlement_asset,
settlement_consume,
settlement_flow,
settlement_incoming_consume,
None,
);
let ordered = match side {
Side::Buy => [settlement_leg, underlying_leg],
Side::Sell => [underlying_leg, settlement_leg],
};
for (kind, asset, consume, flow, incoming_consume, realize_price) in ordered {
self.settle_fill_leg(
account_id,
asset,
kind,
consume,
flow,
incoming_consume,
realize_price,
deltas,
)?;
}
if let Some(fee) = fee {
self.apply_fee_debit(account_id, underlying_asset, settlement_asset, fee, deltas)?;
}
if let (Some(account_currency), Some(barrier)) =
(account_currency.as_ref(), pnl_barrier.as_ref())
{
let mut pnl_delta = deltas.underlying.pnl_delta.unwrap_or(Pnl::ZERO);
if let Some(fee_delta) = fee_pnl_delta {
pnl_delta = pnl_delta.checked_add(fee_delta).map_err(|_| {
self.pnl_arithmetic_failed_block(format!(
"spot-funds fill pnl + fee pnl overflow: pnl {pnl_delta}, \
fee pnl {fee_delta}, account currency {account_currency}, \
account {account_id}"
))
})?;
}
if let Some(block) =
self.apply_account_pnl_delta(account_id, account_currency, barrier, pnl_delta)
{
return Err(block);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn settle_fill_leg(
&self,
account_id: AccountId,
asset: &Asset,
kind: LegKind,
consume: PositionSize,
flow_received: PositionSize,
incoming_consume: PositionSize,
realize_price: Option<Price>,
deltas: &mut FillCancelDeltas,
) -> Result<(), AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let balance_credit = consume.checked_add(flow_received).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fill balance credit overflow: account {account_id}, asset {asset}, \
consume {consume}, flow {flow_received}"
),
)
})?;
if consume.is_zero() && balance_credit.is_zero() && incoming_consume.is_zero() {
return Ok(());
}
let mut pnl_delta = None;
let new_h = self
.mutate_slot((account_id, asset.clone()), |h| {
let realized = match (kind, realize_price) {
(LegKind::Underlying, Some(price)) => {
let (with_pnl, delta) = h.realize_position_fill(flow_received, price)?;
pnl_delta = delta;
with_pnl
}
(LegKind::Underlying, None) if flow_received.is_zero() => h,
(LegKind::Underlying, None) => h.without_position_tracking(),
(LegKind::Settlement, _) => h,
};
let after_outflow = realized.apply_fill_outflow(consume)?;
let after_credit = if balance_credit.is_zero() {
after_outflow
} else {
after_outflow.apply_fill_inflow(balance_credit)?
};
if incoming_consume.is_zero() {
Ok(after_credit)
} else {
after_credit.consume_incoming(incoming_consume)
}
})
.map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fill leg mutation overflow: account {account_id}, asset {asset}, \
consume {consume}, credit {balance_credit}"
),
)
})?;
let leg = deltas.leg_mut(kind);
leg.held_delta = leg.held_delta.checked_sub(consume).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fill held delta overflow: account {account_id}, asset {asset}, \
consume {consume}"
),
)
})?;
leg.incoming_delta = leg
.incoming_delta
.checked_sub(incoming_consume)
.map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fill incoming delta overflow: account {account_id}, asset {asset}, \
incoming {incoming_consume}"
),
)
})?;
leg.balance_delta = leg.balance_delta.checked_add(balance_credit).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fill balance delta overflow: account {account_id}, asset {asset}, \
credit {balance_credit}"
),
)
})?;
if let Some(pnl_delta) = pnl_delta {
leg.pnl_delta = Some(match leg.pnl_delta {
Some(current) => current.checked_add(pnl_delta).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"fill pnl delta overflow: account {account_id}, asset {asset}, \
pnl {pnl_delta}"
),
)
})?,
None => pnl_delta,
});
}
leg.final_holdings = Some(new_h);
Ok(())
}
fn settlement_fill_consume(
&self,
account_id: AccountId,
settlement_asset: &Asset,
side: Side,
trade: Trade,
lock: &PreTradeLock,
) -> Result<PositionSize, AccountBlock> {
let lock_price =
settlement_lock_price(Self::NAME, lock, self.group_id(), "settlement fill")?;
settlement_reserved_amount(
Self::NAME,
side,
lock_price,
trade.quantity,
account_id,
settlement_asset,
)
}
fn settlement_incoming_amount(
&self,
account_id: AccountId,
settlement_asset: &Asset,
side: Side,
trade: Trade,
lock: &PreTradeLock,
) -> Result<PositionSize, AccountBlock> {
let lock_price = settlement_lock_price(Self::NAME, lock, self.group_id(), "sell fill")?;
settlement_incoming_proceeds(
Self::NAME,
side,
lock_price,
trade.quantity,
account_id,
settlement_asset,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_cancel_release(
&self,
account_id: AccountId,
underlying_asset: &Asset,
settlement_asset: &Asset,
side: Side,
leaves_quantity: Quantity,
lock: &PreTradeLock,
deltas: &mut FillCancelDeltas,
) -> Result<(), AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let settlement_held_release =
self.settlement_release(account_id, settlement_asset, side, leaves_quantity, lock)?;
let settlement_incoming_release = self.settlement_incoming_release(
account_id,
settlement_asset,
side,
leaves_quantity,
lock,
)?;
let underlying_held_release = match side {
Side::Buy => PositionSize::ZERO,
Side::Sell => leaves_quantity.to_position_size(),
};
let underlying_incoming_release = match side {
Side::Buy => leaves_quantity.to_position_size(),
Side::Sell => PositionSize::ZERO,
};
self.release_leg(
account_id,
underlying_asset,
LegKind::Underlying,
underlying_held_release,
underlying_incoming_release,
deltas,
)?;
self.release_leg(
account_id,
settlement_asset,
LegKind::Settlement,
settlement_held_release,
settlement_incoming_release,
deltas,
)?;
Ok(())
}
fn settlement_release(
&self,
account_id: AccountId,
settlement_asset: &Asset,
side: Side,
leaves_quantity: Quantity,
lock: &PreTradeLock,
) -> Result<PositionSize, AccountBlock> {
let lock_price =
settlement_lock_price(Self::NAME, lock, self.group_id(), "settlement release")?;
settlement_reserved_amount(
Self::NAME,
side,
lock_price,
leaves_quantity,
account_id,
settlement_asset,
)
}
fn settlement_incoming_release(
&self,
account_id: AccountId,
settlement_asset: &Asset,
side: Side,
leaves_quantity: Quantity,
lock: &PreTradeLock,
) -> Result<PositionSize, AccountBlock> {
let lock_price = settlement_lock_price(Self::NAME, lock, self.group_id(), "sell release")?;
settlement_incoming_proceeds(
Self::NAME,
side,
lock_price,
leaves_quantity,
account_id,
settlement_asset,
)
}
fn release_leg(
&self,
account_id: AccountId,
asset: &Asset,
kind: LegKind,
held_release: PositionSize,
incoming_release: PositionSize,
deltas: &mut FillCancelDeltas,
) -> Result<(), AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
if held_release.is_zero() && incoming_release.is_zero() {
return Ok(());
}
let new_h = self
.mutate_slot((account_id, asset.clone()), |h| {
let after_held = h.release(held_release)?;
if incoming_release.is_zero() {
Ok(after_held)
} else {
after_held.consume_incoming(incoming_release)
}
})
.map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"cancel release overflow: account {account_id}, asset {asset}, \
held {held_release}, incoming {incoming_release}"
),
)
})?;
let leg = deltas.leg_mut(kind);
leg.held_delta = leg.held_delta.checked_sub(held_release).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"cancel held delta overflow: account {account_id}, asset {asset}, \
release {held_release}"
),
)
})?;
leg.balance_delta = leg.balance_delta.checked_add(held_release).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"cancel balance delta overflow: account {account_id}, asset {asset}, \
release {held_release}"
),
)
})?;
leg.incoming_delta = leg
.incoming_delta
.checked_sub(incoming_release)
.map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"cancel incoming delta overflow: account {account_id}, asset {asset}, \
release {incoming_release}"
),
)
})?;
leg.final_holdings = Some(new_h);
Ok(())
}
pub(super) fn apply_execution_report_impl<ExecutionReport>(
&self,
ctx: &PostTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
report: &ExecutionReport,
) -> Option<PostTradeResult>
where
ExecutionReport: HasInstrument
+ HasAccountId
+ HasSide
+ HasExecutionReportLastTrade
+ HasExecutionReportFillFee
+ HasLeavesQuantity
+ HasExecutionReportIsFinal
+ HasPreTradeLock,
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
let request = match self.read_execution_request(report) {
Ok(v) => v,
Err(block) => return Some(PostTradeResult::blocks_only(vec![block])),
};
let underlying_asset = request.instrument.underlying_asset().clone();
let settlement_asset = request.instrument.settlement_asset().clone();
let mut account_blocks: Vec<AccountBlock> = Vec::new();
let mut deltas = FillCancelDeltas::new();
if let Some(trade) = request.last_trade {
if let Err(block) = self.apply_trade_fill(
ctx,
request.account_id,
&underlying_asset,
&settlement_asset,
request.side,
trade,
request.fee.as_ref(),
&request.lock,
&mut deltas,
) {
account_blocks.push(block);
}
}
if request.is_final && !request.leaves_quantity.is_zero() {
if let Err(block) = self.apply_cancel_release(
request.account_id,
&underlying_asset,
&settlement_asset,
request.side,
request.leaves_quantity,
&request.lock,
&mut deltas,
) {
account_blocks.push(block);
}
}
let group_id = self.group_id();
let mut adjustments: Vec<AccountAdjustmentOutcome> = Vec::with_capacity(2);
push_leg_outcome(
&mut adjustments,
group_id,
underlying_asset,
&deltas.underlying,
LegKind::Underlying,
);
push_leg_outcome(
&mut adjustments,
group_id,
settlement_asset,
&deltas.settlement,
LegKind::Settlement,
);
if let Some((fee_asset, fee_delta)) = deltas.fee {
push_leg_outcome(
&mut adjustments,
group_id,
fee_asset,
&fee_delta,
LegKind::Settlement,
);
}
if account_blocks.is_empty() && adjustments.is_empty() {
None
} else {
Some(PostTradeResult {
account_blocks,
account_adjustments: adjustments,
})
}
}
}
pub(super) fn single_lock_price(
policy: &str,
lock: &PreTradeLock,
group_id: PolicyGroupId,
purpose: &str,
) -> Result<Price, AccountBlock> {
match optional_lock_price(policy, lock, group_id, purpose)? {
Some(price) => Ok(price),
None => Err(AccountBlock::new(
policy,
RejectCode::MissingRequiredField,
format!("pre-trade lock has no price for {purpose}"),
format!("group {}", group_id.value()),
)),
}
}
pub(super) fn optional_lock_price(
policy: &str,
lock: &PreTradeLock,
group_id: PolicyGroupId,
purpose: &str,
) -> Result<Option<Price>, AccountBlock> {
let mut iter = lock.prices_of(group_id);
match (iter.next(), iter.next()) {
(Some(p), None) => Ok(Some(p)),
(None, _) => Ok(None),
(Some(_), Some(_)) => Err(AccountBlock::new(
policy,
RejectCode::Other,
format!(
"pre-trade lock has multiple prices for {purpose}; \
two SpotFundsPolicies share a group_id"
),
format!("group {}", group_id.value()),
)),
}
}
fn settlement_lock_price(
policy: &str,
lock: &PreTradeLock,
group_id: PolicyGroupId,
purpose: &str,
) -> Result<Option<Price>, AccountBlock> {
Ok(Some(single_lock_price(policy, lock, group_id, purpose)?))
}
fn settlement_reserved_amount(
policy: &str,
side: Side,
lock_price: Option<Price>,
quantity: Quantity,
account_id: AccountId,
settlement_asset: &Asset,
) -> Result<PositionSize, AccountBlock> {
let Some(price) = lock_price else {
return Ok(PositionSize::ZERO);
};
let notional = price.calculate_position_size(quantity).map_err(|_| {
arithmetic_overflow_account_block(
policy,
format!(
"settlement notional overflow: account {account_id}, \
asset {settlement_asset}, lock_px {price}, qty {quantity}"
),
)
})?;
let outflow = match side {
Side::Buy => notional,
Side::Sell => neg(notional),
};
Ok(non_negative(outflow))
}
fn settlement_incoming_proceeds(
policy: &str,
side: Side,
lock_price: Option<Price>,
quantity: Quantity,
account_id: AccountId,
settlement_asset: &Asset,
) -> Result<PositionSize, AccountBlock> {
let Side::Sell = side else {
return Ok(PositionSize::ZERO);
};
let Some(price) = lock_price else {
return Ok(PositionSize::ZERO);
};
let notional = price.calculate_position_size(quantity).map_err(|_| {
arithmetic_overflow_account_block(
policy,
format!(
"settlement proceeds overflow: account {account_id}, \
asset {settlement_asset}, lock_px {price}, qty {quantity}"
),
)
})?;
Ok(non_negative(notional))
}
fn non_negative(value: PositionSize) -> PositionSize {
value.max(PositionSize::ZERO)
}
fn neg(value: PositionSize) -> PositionSize {
-value
}
fn trade_price_with_factor(
policy_name: &str,
price: Price,
factor: Decimal,
) -> Result<Price, AccountBlock> {
price
.to_decimal()
.checked_mul(factor)
.map(Price::new)
.ok_or_else(|| {
arithmetic_overflow_account_block(
policy_name,
format!("account-currency price conversion overflow: px {price}, factor {factor}"),
)
})
}
fn push_leg_outcome(
adjustments: &mut Vec<AccountAdjustmentOutcome>,
group_id: PolicyGroupId,
asset: Asset,
leg: &LegDelta,
kind: LegKind,
) {
if let Some(h) = leg.final_holdings {
let (realized_pnl, average_entry_price) = match kind {
LegKind::Underlying => match (leg.pnl_delta, h.realized_pnl()) {
(Some(delta), Some(absolute)) => {
(nonzero_pnl_outcome(delta, absolute), h.avg_entry_price())
}
_ => (None, None),
},
LegKind::Settlement => (None, None),
};
adjustments.push(AccountAdjustmentOutcome {
policy_group_id: group_id,
entry: AccountOutcomeEntry {
asset,
balance: nonzero_outcome(leg.balance_delta, h.available()),
held: nonzero_outcome(leg.held_delta, h.held()),
incoming: nonzero_outcome(leg.incoming_delta, h.incoming()),
realized_pnl,
average_entry_price,
},
});
}
}
fn nonzero_outcome(delta: PositionSize, absolute: PositionSize) -> Option<OutcomeAmount> {
if delta.is_zero() {
None
} else {
Some(OutcomeAmount { delta, absolute })
}
}
fn nonzero_pnl_outcome(delta: Pnl, absolute: Pnl) -> Option<PnlOutcomeAmount> {
if delta.is_zero() {
None
} else {
Some(PnlOutcomeAmount { delta, absolute })
}
}