use crate::core::account_outcome::{AccountAdjustmentOutcome, OutcomeAmount};
use crate::core::sync_mode::SyncMode;
use crate::core::{
AccountOutcomeEntry, HasAccountId, HasExecutionReportIsFinal, HasExecutionReportLastTrade,
HasInstrument, HasLeavesQuantity, HasPreTradeLock, HasSide,
};
use crate::marketdata::MarketDataSync;
use crate::param::{AccountId, Asset, 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, PostTradeResult, PreTradeLock, RejectCode};
use super::rejects::arithmetic_overflow_account_block;
use super::views::{ExecutionRequestView, FillCancelDeltas, LegDelta, LegKind};
use super::{HoldingsKey, SpotFundsPolicy};
impl<Sync, MarketDataSyncMode> SpotFundsPolicy<Sync, MarketDataSyncMode>
where
Sync: SyncMode,
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)
}
pub(super) fn release_held(
&self,
account_id: AccountId,
asset: &Asset,
amount: PositionSize,
) -> Result<Holdings, AdjustmentOverflowError>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
self.mutate_slot((account_id, asset.clone()), |h| h.release(amount))
}
pub(super) fn read_execution_request<'i, ExecutionReport>(
&self,
report: &'i ExecutionReport,
) -> Result<ExecutionRequestView<'i>, AccountBlock>
where
ExecutionReport: HasInstrument
+ HasAccountId
+ HasSide
+ HasExecutionReportLastTrade
+ 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 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,
leaves_quantity,
is_final,
lock,
})
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_trade_fill(
&self,
account_id: AccountId,
underlying_asset: &Asset,
settlement_asset: &Asset,
side: Side,
trade: Trade,
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 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_leg = (
LegKind::Underlying,
underlying_asset,
underlying_consume,
underlying_flow,
);
let settlement_leg = (
LegKind::Settlement,
settlement_asset,
settlement_consume,
settlement_flow,
);
let ordered = match side {
Side::Buy => [settlement_leg, underlying_leg],
Side::Sell => [underlying_leg, settlement_leg],
};
for (kind, asset, consume, flow) in ordered {
self.settle_fill_leg(account_id, asset, kind, consume, flow, deltas)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn settle_fill_leg(
&self,
account_id: AccountId,
asset: &Asset,
kind: LegKind,
consume: PositionSize,
flow_received: PositionSize,
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() {
return Ok(());
}
let new_h = self
.mutate_slot((account_id, asset.clone()), |h| {
let after_outflow = h.apply_fill_outflow(consume)?;
if balance_credit.is_zero() {
Ok(after_outflow)
} else {
after_outflow.apply_fill_inflow(balance_credit)
}
})
.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.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}"
),
)
})?;
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, side, lock, self.group_id, "buy fill")?;
settlement_reserved_amount(
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 underlying_release = match side {
Side::Buy => PositionSize::ZERO,
Side::Sell => leaves_quantity.to_position_size(),
};
self.release_leg(
account_id,
underlying_asset,
LegKind::Underlying,
underlying_release,
deltas,
)?;
let settlement_release =
self.settlement_release(account_id, settlement_asset, side, leaves_quantity, lock)?;
self.release_leg(
account_id,
settlement_asset,
LegKind::Settlement,
settlement_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, side, lock, self.group_id, "buy release")?;
settlement_reserved_amount(
Self::NAME,
side,
lock_price,
leaves_quantity,
account_id,
settlement_asset,
)
}
fn release_leg(
&self,
account_id: AccountId,
asset: &Asset,
kind: LegKind,
release: PositionSize,
deltas: &mut FillCancelDeltas,
) -> Result<(), AccountBlock>
where
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
if release.is_zero() {
return Ok(());
}
let new_h = self.release_held(account_id, asset, release).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"cancel release overflow: account {account_id}, asset {asset}, \
requested {release}"
),
)
})?;
let leg = deltas.leg_mut(kind);
leg.held_delta = leg.held_delta.checked_sub(release).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"cancel held delta overflow: account {account_id}, asset {asset}, \
release {release}"
),
)
})?;
leg.balance_delta = leg.balance_delta.checked_add(release).map_err(|_| {
arithmetic_overflow_account_block(
Self::NAME,
format!(
"cancel balance delta overflow: account {account_id}, asset {asset}, \
release {release}"
),
)
})?;
leg.final_holdings = Some(new_h);
Ok(())
}
pub(super) fn apply_execution_report_impl<ExecutionReport>(
&self,
report: &ExecutionReport,
) -> Option<PostTradeResult>
where
ExecutionReport: HasInstrument
+ HasAccountId
+ HasSide
+ HasExecutionReportLastTrade
+ 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(
request.account_id,
&underlying_asset,
&settlement_asset,
request.side,
trade,
&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 mut adjustments: Vec<AccountAdjustmentOutcome> = Vec::with_capacity(2);
push_leg_outcome(
&mut adjustments,
self.group_id,
underlying_asset,
&deltas.underlying,
);
push_leg_outcome(
&mut adjustments,
self.group_id,
settlement_asset,
&deltas.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,
side: Side,
lock: &PreTradeLock,
group_id: PolicyGroupId,
purpose: &str,
) -> Result<Option<Price>, AccountBlock> {
match side {
Side::Buy => Ok(Some(single_lock_price(policy, lock, group_id, purpose)?)),
Side::Sell => optional_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 non_negative(value: PositionSize) -> PositionSize {
value.max(PositionSize::ZERO)
}
fn neg(value: PositionSize) -> PositionSize {
-value
}
fn push_leg_outcome(
adjustments: &mut Vec<AccountAdjustmentOutcome>,
group_id: PolicyGroupId,
asset: Asset,
leg: &LegDelta,
) {
if let Some(h) = leg.final_holdings {
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: None,
},
});
}
}
fn nonzero_outcome(delta: PositionSize, absolute: PositionSize) -> Option<OutcomeAmount> {
if delta.is_zero() {
None
} else {
Some(OutcomeAmount { delta, absolute })
}
}