use super::*;
use crate::core::AccountAdjustmentContext;
use crate::marketdata::{MarketDataBuilder, Quote, QuoteTtl};
use crate::param::{
AccountId, AdjustmentAmount, Asset, Pnl, PositionSize, Price, Quantity, Side, Trade,
TradeAmount, Volume,
};
use crate::pretrade::{
holdings::Holdings, PreTradeContext, PreTradeLock, PreTradePolicy, RejectCode,
DEFAULT_POLICY_GROUP_ID,
};
use crate::{
FullSync, HasAccountAdjustmentBalance, HasAccountAdjustmentBalanceAverageEntryPrice,
HasAccountAdjustmentBalanceLowerBound, HasAccountAdjustmentBalanceRealizedPnl,
HasAccountAdjustmentBalanceUpperBound, HasAccountAdjustmentHeld,
HasAccountAdjustmentHeldLowerBound, HasAccountAdjustmentHeldUpperBound,
HasAccountAdjustmentIncoming, HasAccountAdjustmentIncomingLowerBound,
HasAccountAdjustmentIncomingUpperBound, HasAccountId, HasBalanceAsset,
HasExecutionReportIsFinal, HasExecutionReportLastTrade, HasInstrument, HasLeavesQuantity,
HasPreTradeLock, HasSide, Instrument, Mutations, OrderOperation, RequestFieldAccessError,
};
use std::sync::Arc;
type TestPolicy = SpotFundsPolicy<FullSync, FullSync>;
type TestOrder = OrderOperation;
struct TestReport {
instrument: Instrument,
account_id: AccountId,
side: Side,
last_trade: Option<Trade>,
leaves_quantity: Quantity,
is_final: bool,
lock: PreTradeLock,
}
impl HasInstrument for TestReport {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
impl HasAccountId for TestReport {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Ok(self.account_id)
}
}
impl HasSide for TestReport {
fn side(&self) -> Result<Side, RequestFieldAccessError> {
Ok(self.side)
}
}
impl HasExecutionReportLastTrade for TestReport {
fn last_trade(&self) -> Result<Option<Trade>, RequestFieldAccessError> {
Ok(self.last_trade)
}
}
impl HasLeavesQuantity for TestReport {
fn leaves_quantity(&self) -> Result<Quantity, RequestFieldAccessError> {
Ok(self.leaves_quantity)
}
}
impl HasExecutionReportIsFinal for TestReport {
fn is_final(&self) -> Result<bool, RequestFieldAccessError> {
Ok(self.is_final)
}
}
impl HasPreTradeLock for TestReport {
fn lock(&self) -> Result<PreTradeLock, RequestFieldAccessError> {
Ok(self.lock.clone())
}
}
struct TestAdjustment {
asset: Asset,
balance: Option<AdjustmentAmount>,
balance_average_entry_price: Option<Price>,
balance_realized_pnl: Option<Pnl>,
balance_lower: Option<PositionSize>,
balance_upper: Option<PositionSize>,
held: Option<AdjustmentAmount>,
held_lower: Option<PositionSize>,
held_upper: Option<PositionSize>,
incoming: Option<AdjustmentAmount>,
incoming_lower: Option<PositionSize>,
incoming_upper: Option<PositionSize>,
}
impl HasBalanceAsset for TestAdjustment {
fn balance_asset(&self) -> Result<&Asset, RequestFieldAccessError> {
Ok(&self.asset)
}
}
impl HasAccountAdjustmentBalance for TestAdjustment {
fn balance(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(self.balance)
}
}
impl HasAccountAdjustmentBalanceAverageEntryPrice for TestAdjustment {
fn balance_average_entry_price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(self.balance_average_entry_price)
}
}
impl HasAccountAdjustmentBalanceRealizedPnl for TestAdjustment {
fn balance_realized_pnl(&self) -> Result<Option<Pnl>, RequestFieldAccessError> {
Ok(self.balance_realized_pnl)
}
}
impl HasAccountAdjustmentBalanceLowerBound for TestAdjustment {
fn balance_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.balance_lower)
}
}
impl HasAccountAdjustmentBalanceUpperBound for TestAdjustment {
fn balance_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.balance_upper)
}
}
impl HasAccountAdjustmentHeld for TestAdjustment {
fn held(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(self.held)
}
}
impl HasAccountAdjustmentHeldLowerBound for TestAdjustment {
fn held_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.held_lower)
}
}
impl HasAccountAdjustmentHeldUpperBound for TestAdjustment {
fn held_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.held_upper)
}
}
impl HasAccountAdjustmentIncoming for TestAdjustment {
fn incoming(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(self.incoming)
}
}
impl HasAccountAdjustmentIncomingLowerBound for TestAdjustment {
fn incoming_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.incoming_lower)
}
}
impl HasAccountAdjustmentIncomingUpperBound for TestAdjustment {
fn incoming_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.incoming_upper)
}
}
fn asset(s: &str) -> Asset {
Asset::new(s).expect("valid asset")
}
fn ps(s: &str) -> PositionSize {
PositionSize::from_str(s).expect("valid position size")
}
fn pnl_value(s: &str) -> Pnl {
Pnl::from_str(s).expect("valid pnl")
}
fn px(s: &str) -> Price {
Price::from_str(s).expect("valid price")
}
fn qty(s: &str) -> Quantity {
Quantity::from_str(s).expect("valid quantity")
}
fn vol(s: &str) -> Volume {
Volume::from_str(s).expect("valid volume")
}
fn account(n: u64) -> AccountId {
AccountId::from_u64(n)
}
fn dummy_control(
account_id: AccountId,
) -> crate::core::AccountControl<crate::storage::FullLocking> {
use crate::core::account_control::BlockedAccounts;
use crate::core::AccountBlockHandle;
use crate::storage::{FullLocking, LockingPolicyFactory, StorageBuilder};
let sb = StorageBuilder::new(FullLocking);
let blocked = FullLocking::new_shared(BlockedAccounts::new(&sb));
let handle = AccountBlockHandle::from_inner(blocked);
crate::core::AccountControl::new(handle, account_id)
}
fn instr(under: &str, sett: &str) -> Instrument {
Instrument::new(asset(under), asset(sett))
}
fn engine_builder() -> crate::SyncedEngineBuilder<(), (), (), crate::FullSync> {
crate::Engine::builder().full_sync()
}
fn settings(slip_bps: u16) -> SpotFundsSettings {
SpotFundsSettings::new(slip_bps, SpotFundsPricingSource::Mark, std::iter::empty())
.expect("settings must build")
}
fn build_policy(_mark: Option<()>, _slip_bps: Option<u16>) -> TestPolicy {
let b = engine_builder();
SpotFundsPolicy::new(settings(0), None, b.storage_builder())
}
fn build_policy_with_market_data(
instrument: Instrument,
price: Price,
slip_bps: u16,
) -> TestPolicy {
let b = engine_builder();
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let id = svc
.register(instrument.clone())
.expect("register must succeed");
svc.push(id, Quote::new().with_mark(price))
.expect("push must succeed");
let bundle = SpotFundsMarketData::new(Arc::clone(&svc));
SpotFundsPolicy::new(settings(slip_bps), Some(bundle), b.storage_builder())
}
fn make_order(
account_id: AccountId,
instrument: Instrument,
side: Side,
trade_amount: TradeAmount,
price: Option<Price>,
) -> TestOrder {
OrderOperation {
instrument,
account_id,
side,
trade_amount,
price,
}
}
fn make_report(
account_id: AccountId,
instrument: Instrument,
side: Side,
last_trade: Option<Trade>,
leaves: Quantity,
is_final: bool,
lock: Option<PreTradeLock>,
) -> TestReport {
TestReport {
instrument,
account_id,
side,
last_trade,
leaves_quantity: leaves,
is_final,
lock: lock.unwrap_or_default(),
}
}
fn adj(asset: Asset, balance: Option<AdjustmentAmount>) -> TestAdjustment {
TestAdjustment {
asset,
balance,
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
}
}
fn adj_with_avg(
asset: Asset,
balance: Option<AdjustmentAmount>,
average_entry_price: Option<Price>,
) -> TestAdjustment {
TestAdjustment {
asset,
balance,
balance_average_entry_price: average_entry_price,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
}
}
fn adj_with_realized_pnl(
asset: Asset,
balance: Option<AdjustmentAmount>,
realized_pnl: Option<Pnl>,
) -> TestAdjustment {
TestAdjustment {
asset,
balance,
balance_average_entry_price: None,
balance_realized_pnl: realized_pnl,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
}
}
fn bounded_adj(
asset: Asset,
balance: Option<AdjustmentAmount>,
lower: Option<PositionSize>,
upper: Option<PositionSize>,
) -> TestAdjustment {
TestAdjustment {
asset,
balance,
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: lower,
balance_upper: upper,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
}
}
fn held_adj(
asset: Asset,
held: Option<AdjustmentAmount>,
lower: Option<PositionSize>,
upper: Option<PositionSize>,
) -> TestAdjustment {
TestAdjustment {
asset,
balance: None,
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held,
held_lower: lower,
held_upper: upper,
incoming: None,
incoming_lower: None,
incoming_upper: None,
}
}
fn incoming_adj(
asset: Asset,
incoming: Option<AdjustmentAmount>,
lower: Option<PositionSize>,
upper: Option<PositionSize>,
) -> TestAdjustment {
TestAdjustment {
asset,
balance: None,
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming,
incoming_lower: lower,
incoming_upper: upper,
}
}
fn all_fields_adj(
asset: Asset,
balance: Option<AdjustmentAmount>,
held: Option<AdjustmentAmount>,
incoming: Option<AdjustmentAmount>,
) -> TestAdjustment {
TestAdjustment {
asset,
balance,
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held,
held_lower: None,
held_upper: None,
incoming,
incoming_lower: None,
incoming_upper: None,
}
}
fn seed(policy: &TestPolicy, account_id: AccountId, asset: Asset, amount: &str) {
let adjustment = adj(asset, Some(AdjustmentAmount::Absolute(ps(amount))));
let mut mutations = Mutations::with_capacity(1);
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_account_adjustment(
policy,
&AccountAdjustmentContext::new_test(dummy_control(account_id)),
account_id,
&adjustment,
&mut mutations,
)
.expect("seed must succeed");
mutations.commit_all();
}
fn holdings_of(policy: &TestPolicy, account_id: AccountId, asset: &Asset) -> Option<Holdings> {
policy.holdings.get(&(account_id, asset.clone()))
}
fn pre_trade_check(
policy: &TestPolicy,
order: &TestOrder,
mutations: &mut Mutations,
) -> Result<(), crate::pretrade::Rejects> {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::perform_pre_trade_check(
policy,
&PreTradeContext::new(None),
order,
mutations,
)
.map(|_| ())
}
fn dry_run_check(
policy: &TestPolicy,
order: &TestOrder,
) -> Result<Option<crate::pretrade::PolicyPreTradeResult>, crate::pretrade::Rejects> {
let mut mutations = Mutations::new();
let result = <TestPolicy as PreTradePolicy<
TestOrder,
TestReport,
TestAdjustment,
crate::core::FullSync,
>>::perform_pre_trade_check_dry_run(
policy, &PreTradeContext::new(None), order, &mut mutations
);
assert!(mutations.is_empty(), "dry-run must push no mutations");
result
}
fn apply_adj(
policy: &TestPolicy,
account_id: AccountId,
adjustment: &TestAdjustment,
mutations: &mut Mutations,
) -> Result<(), crate::pretrade::Rejects> {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_account_adjustment(
policy,
&AccountAdjustmentContext::new_test(dummy_control(account_id)),
account_id,
adjustment,
mutations,
)
.map(|_| ())
}
fn report_blocks(policy: &TestPolicy, report: &TestReport) -> Vec<crate::pretrade::AccountBlock> {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_execution_report(
policy,
&post_trade_ctx(report),
report,
)
.map(|r| r.account_blocks)
.unwrap_or_default()
}
fn post_trade_ctx(
report: &TestReport,
) -> crate::pretrade::PostTradeContext<crate::storage::FullLocking> {
crate::pretrade::PostTradeContext::with_account_currency(
report.account_id,
report.instrument.settlement_asset().clone(),
)
}
#[test]
fn new_creates_empty_holdings() {
let policy = build_policy(None, None);
assert!(holdings_of(&policy, account(99224416), &asset("USD")).is_none());
}
#[test]
fn settings_new_rejects_out_of_range_global_slippage() {
let result = SpotFundsSettings::new(10_001, SpotFundsPricingSource::Mark, std::iter::empty());
assert_eq!(
result.err(),
Some(SpotFundsConfigError::SlippageOutOfRange { bps: 10_001 })
);
}
#[test]
fn settings_new_accepts_max_slippage_boundary() {
assert!(
SpotFundsSettings::new(10_000, SpotFundsPricingSource::Mark, std::iter::empty()).is_ok()
);
}
#[test]
fn settings_set_global_slippage_bps_boundary_and_reject() {
let mut s = settings(0);
assert!(s.set_global_slippage_bps(10_000).is_ok());
assert_eq!(
s.set_global_slippage_bps(10_001).err(),
Some(SpotFundsConfigError::SlippageOutOfRange { bps: 10_001 })
);
let (svc, id) = {
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let id = svc
.register(instr("AAPL", "USD"))
.expect("register must succeed");
svc.push(id, Quote::new().with_mark(px("100")))
.expect("push must succeed");
(svc, id)
};
let md = SpotFundsMarketData::<FullSync>::new(Arc::clone(&svc));
let quote = md.quote(id, account(7), &None).expect("quote present");
assert!(s
.effective_sell_price("e, id, account(7), &None)
.is_err());
}
#[test]
fn settings_set_override_above_bound_is_rejected() {
let mut s = settings(0);
let id = {
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
svc.register(instr("AAPL", "USD"))
.expect("register must succeed")
};
assert_eq!(
s.set_override(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(10_001),
},
)
.err(),
Some(SpotFundsConfigError::SlippageOutOfRange { bps: 10_001 })
);
}
#[test]
fn settings_set_override_then_clear_falls_back_to_global() {
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let id = svc
.register(instr("AAPL", "USD"))
.expect("register must succeed");
svc.push(id, Quote::new().with_mark(px("100")))
.expect("push must succeed");
let md = SpotFundsMarketData::<FullSync>::new(Arc::clone(&svc));
let quote = md.quote(id, account(7), &None).expect("quote present");
let mut s = settings(0);
s.set_override(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride {
slippage_bps: Some(1000),
},
)
.expect("override must set");
assert_eq!(
s.effective_buy_price("e, id, account(7), &None),
Ok(px("110"))
);
s.set_override(
SpotFundsOverrideTarget::Instrument(id),
SpotFundsOverride { slippage_bps: None },
)
.expect("override must clear");
assert_eq!(
s.effective_buy_price("e, id, account(7), &None),
Ok(px("100"))
);
}
#[test]
fn settings_set_pricing_source_switches_quote_field() {
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let id = svc
.register(instr("AAPL", "USD"))
.expect("register must succeed");
svc.push(id, Quote::new().with_ask(px("100")))
.expect("push must succeed");
let md = SpotFundsMarketData::<FullSync>::new(Arc::clone(&svc));
let quote = md.quote(id, account(7), &None).expect("quote present");
let mut s = settings(0);
assert!(s
.effective_buy_price("e, id, account(7), &None)
.is_err());
s.set_pricing_source(SpotFundsPricingSource::BookTop);
assert_eq!(
s.effective_buy_price("e, id, account(7), &None),
Ok(px("100"))
);
}
#[test]
fn with_policy_group_id_records_tag_observed_by_policy() {
use crate::pretrade::PreTradePolicy;
let id = DEFAULT_POLICY_GROUP_ID;
let policy = build_policy(None, None);
assert_eq!(
<TestPolicy as PreTradePolicy<
TestOrder,
TestReport,
TestAdjustment,
crate::core::FullSync,
>>::policy_group_id(&policy),
id
);
let tag = crate::pretrade::PolicyGroupId::new(7);
let tagged = build_policy(None, None).with_policy_group_id(tag);
assert_eq!(
<TestPolicy as PreTradePolicy<
TestOrder,
TestReport,
TestAdjustment,
crate::core::FullSync,
>>::policy_group_id(&tagged),
tag
);
}
#[test]
fn settings_cell_clone_shares_state_with_running_policy() {
use crate::pretrade::ConfigurablePolicy;
use crate::storage::ConfigCell;
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("100"), 0);
seed(&policy, acc, asset("USD"), "10000");
let cell =
<TestPolicy as ConfigurablePolicy<crate::storage::FullLocking>>::settings_cell(&policy);
cell.update(|s| s.set_global_slippage_bps(2000))
.expect("update must publish");
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("1200"));
assert_eq!(h.available(), ps("8800"));
}
#[test]
fn buy_qty_limit_sufficient_reserves_settlement() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
assert!(pre_trade_check(&policy, &order, &mut mutations).is_ok());
assert!(!mutations.is_empty());
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("2000"));
assert_eq!(h.available(), ps("8000"));
}
#[test]
fn buy_qty_limit_insufficient_rejects_insufficient_funds() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "1000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
assert!(mutations.is_empty());
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("1000"));
assert_eq!(h.held(), ps("0"));
}
#[test]
fn dry_run_buy_reports_outcome_and_leaves_holdings_untouched() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let outcome = dry_run_check(&policy, &order)
.expect("dry-run must pass")
.expect("dry-run must report an outcome");
assert_eq!(outcome.account_adjustments.len(), 1);
let entry = &outcome.account_adjustments[0];
assert_eq!(entry.asset, asset("USD"));
let held = entry.held.expect("held outcome present");
assert_eq!(held.delta, ps("2000"));
assert_eq!(held.absolute, ps("2000"));
let balance = entry.balance.expect("balance outcome present");
assert_eq!(balance.delta, ps("-2000"));
assert_eq!(balance.absolute, ps("8000"));
assert_eq!(outcome.lock_prices.to_vec(), vec![px("200")]);
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("10000"));
assert_eq!(h.held(), ps("0"));
}
#[test]
fn dry_run_buy_matches_real_reservation_holdings_then_leaves_them_for_the_real_call() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
dry_run_check(&policy, &order).expect("first dry-run must pass");
dry_run_check(&policy, &order).expect("second dry-run must pass");
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("10000"));
assert_eq!(h.held(), ps("0"));
let mut mutations = Mutations::with_capacity(1);
assert!(pre_trade_check(&policy, &order, &mut mutations).is_ok());
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("2000"));
assert_eq!(h.available(), ps("8000"));
}
#[test]
fn dry_run_buy_insufficient_reports_same_reject_and_leaves_holdings_untouched() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "1000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let rejects = dry_run_check(&policy, &order).expect_err("dry-run must reject");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
dry_run_check(&policy, &order).expect_err("dry-run must reject again");
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("1000"));
assert_eq!(h.held(), ps("0"));
}
#[test]
fn dry_run_sell_reports_underlying_hold_and_leaves_holdings_untouched() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Quantity(qty("4")),
Some(px("200")),
);
let outcome = dry_run_check(&policy, &order)
.expect("dry-run must pass")
.expect("dry-run must report an outcome");
assert_eq!(outcome.account_adjustments.len(), 1);
let entry = &outcome.account_adjustments[0];
assert_eq!(entry.asset, asset("AAPL"));
let held = entry.held.expect("held outcome present");
assert_eq!(held.delta, ps("4"));
assert_eq!(held.absolute, ps("4"));
let h = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(h.available(), ps("10"));
assert_eq!(h.held(), ps("0"));
}
#[test]
fn buy_volume_sufficient_reserves_volume_amount() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Volume(vol("3000")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
assert!(pre_trade_check(&policy, &order, &mut mutations).is_ok());
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("3000"));
assert_eq!(h.available(), ps("7000"));
}
#[test]
fn buy_volume_insufficient_rejects() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "2000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Volume(vol("3000")),
Some(px("200")),
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
}
#[test]
fn buy_volume_without_price_or_mark_rejects_as_unsupported() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Volume(vol("3000")),
None,
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::UnsupportedOrderType);
}
#[test]
fn buy_market_with_mark_reserves_slippage_adjusted_amount() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("200"), 1500);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
assert!(pre_trade_check(&policy, &order, &mut mutations).is_ok());
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("2300"));
assert_eq!(h.available(), ps("7700"));
}
#[test]
fn buy_market_no_bundle_rejects_unsupported_order_type() {
let acc = account(99224416);
let policy = build_policy(None::<()>, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::UnsupportedOrderType);
}
#[test]
fn sell_qty_sufficient_holds_underlying() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Quantity(qty("4")),
None,
);
let mut mutations = Mutations::with_capacity(1);
assert!(pre_trade_check(&policy, &order, &mut mutations).is_ok());
let h = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(h.held(), ps("4"));
assert_eq!(h.available(), ps("6"));
}
#[test]
fn sell_qty_insufficient_rejects() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "3");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Quantity(qty("4")),
None,
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
let h = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(h.available(), ps("3"));
assert_eq!(h.held(), ps("0"));
}
#[test]
fn sell_volume_limit_holds_quantity_charge() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Volume(vol("600")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
assert!(pre_trade_check(&policy, &order, &mut mutations).is_ok());
let h = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(h.held(), ps("3"));
assert_eq!(h.available(), ps("7"));
}
#[test]
fn sell_volume_limit_insufficient_rejects() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "2");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Volume(vol("600")),
Some(px("200")),
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
}
#[test]
fn sell_volume_market_zero_slip_holds_correct_quantity() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("200"), 0);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Volume(vol("400")),
None,
);
let mut mutations = Mutations::with_capacity(1);
assert!(pre_trade_check(&policy, &order, &mut mutations).is_ok());
let h = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(h.held(), ps("2"));
assert_eq!(h.available(), ps("8"));
}
#[test]
fn sell_volume_market_full_slip_rejects_order_value_calculation_failed() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("200"), 10_000);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Volume(vol("400")),
None,
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::OrderValueCalculationFailed);
}
#[test]
fn missing_holdings_treated_as_zero_rejects_insufficient_funds() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "EUR"),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("200")),
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
assert!(mutations.is_empty());
}
#[test]
fn insufficient_funds_on_missing_settlement_does_not_create_holdings_entry() {
let acc = account(99224416);
let policy = build_policy(None, None);
let order = make_order(
acc,
instr("AAPL", "EUR"),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("100")),
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
assert!(
holdings_of(&policy, acc, &asset("EUR")).is_none(),
"phantom entry must not be created on reject"
);
}
#[test]
fn bounds_exceeded_on_new_asset_does_not_create_holdings_entry() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = bounded_adj(
asset("EUR"),
Some(AdjustmentAmount::Delta(ps("10"))),
None,
Some(ps("0")),
);
let mut mutations = Mutations::new();
let rejects = apply_adj(&policy, acc, &adjustment, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::AccountAdjustmentBoundsExceeded);
assert!(
holdings_of(&policy, acc, &asset("EUR")).is_none(),
"phantom entry must not be created on reject"
);
}
#[test]
fn negative_result_on_new_asset_does_not_create_holdings_entry() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = bounded_adj(
asset("EUR"),
Some(AdjustmentAmount::Absolute(ps("-1"))),
Some(ps("0")),
None,
);
let mut mutations = Mutations::new();
let rejects = apply_adj(&policy, acc, &adjustment, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::AccountAdjustmentBoundsExceeded);
assert!(
holdings_of(&policy, acc, &asset("EUR")).is_none(),
"phantom entry must not be created on reject"
);
}
#[test]
fn rollback_restores_holdings_to_pre_reserve_state() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
let after_check = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(after_check.held(), ps("2000"));
assert_eq!(after_check.available(), ps("8000"));
mutations.rollback_all();
let after_rollback = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(after_rollback.held(), ps("0"));
assert_eq!(after_rollback.available(), ps("10000"));
}
#[test]
fn concurrent_second_check_rejects_when_first_already_reserved() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "100");
let order_a = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Volume(vol("100")),
Some(px("1")),
);
let order_b = make_order(
acc,
instr("AAPL", "USD"),
Side::Buy,
TradeAmount::Volume(vol("100")),
Some(px("1")),
);
let mut mutations_a = Mutations::with_capacity(1);
pre_trade_check(&policy, &order_a, &mut mutations_a).expect("A must pass");
let mut mutations_b = Mutations::new();
let rejects = pre_trade_check(&policy, &order_b, &mut mutations_b)
.expect_err("B must reject - funds already held by A");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
mutations_a.rollback_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("0"));
assert_eq!(h.available(), ps("100"));
}
#[test]
fn buy_partial_fill_consumes_held_settlement_and_credits_underlying() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let report = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let blocks = report_blocks(&policy, &report);
assert!(blocks.is_empty());
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("1200")); assert_eq!(usd.available(), ps("8000"));
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("AAPL entry created");
assert_eq!(aapl.available(), ps("4"));
assert_eq!(aapl.held(), ps("0"));
}
#[test]
fn sell_partial_fill_consumes_held_underlying_and_credits_settlement() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let report = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
None,
);
report_blocks(&policy, &report);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.held(), ps("6")); assert_eq!(aapl.available(), ps("0"));
let usd = holdings_of(&policy, acc, &asset("USD")).expect("USD entry created");
assert_eq!(usd.available(), ps("800")); }
#[test]
fn buy_fill_without_lock_price_blocks_account() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
None,
);
let blocks = report_blocks(&policy, &fill);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, RejectCode::MissingRequiredField);
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("2000"));
assert_eq!(usd.available(), ps("8000"));
}
#[test]
fn buy_fill_with_multiple_lock_prices_blocks_account() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([
(DEFAULT_POLICY_GROUP_ID, px("200")),
(DEFAULT_POLICY_GROUP_ID, px("210")),
])),
);
let blocks = report_blocks(&policy, &fill);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, RejectCode::Other);
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("2000"));
assert_eq!(usd.available(), ps("8000"));
}
#[test]
fn buy_limit_cancel_leftover_releases_held_by_leaves_times_price() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
report_blocks(&policy, &fill);
let cancel = make_report(
acc,
aapl_usd,
Side::Buy,
None,
qty("6"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
report_blocks(&policy, &cancel);
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("0"));
assert_eq!(usd.available(), ps("9200"));
}
#[test]
fn buy_market_cancel_uses_lock_price_for_release() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("200"), 1500);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("195"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("230"),
)])),
);
report_blocks(&policy, &fill);
let cancel = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("195"),
quantity: qty("0"),
}),
qty("6"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("230"),
)])),
);
report_blocks(&policy, &cancel);
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("0"));
assert_eq!(usd.available(), ps("9220"));
}
#[test]
fn buy_market_cancel_without_lock_price_blocks() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("200"), 1500);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let cancel = make_report(acc, aapl_usd, Side::Buy, None, qty("10"), true, None);
let blocks = report_blocks(&policy, &cancel);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, RejectCode::MissingRequiredField);
}
#[test]
fn buy_market_cancel_with_multiple_lock_prices_blocks() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("200"), 1500);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let cancel = make_report(
acc,
aapl_usd,
Side::Buy,
None,
qty("10"),
true,
Some(PreTradeLock::from_entries([
(DEFAULT_POLICY_GROUP_ID, px("230")),
(DEFAULT_POLICY_GROUP_ID, px("240")),
])),
);
let blocks = report_blocks(&policy, &cancel);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, RejectCode::Other);
}
#[test]
fn buy_market_cancel_no_fills_with_lock_price_releases_full_amount() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("200"), 1500);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let cancel = make_report(
acc,
aapl_usd,
Side::Buy,
None,
qty("10"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("230"),
)])),
);
report_blocks(&policy, &cancel);
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("0"));
assert_eq!(usd.available(), ps("10000"));
}
#[test]
fn buy_market_cancel_no_fills_no_mark_held_stays_stuck() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let usd = asset("USD");
policy
.holdings
.with_mut((acc, usd.clone()), Holdings::zero, |slot, _| {
*slot = Holdings::new(ps("0"), ps("2300"));
});
let cancel = make_report(acc, aapl_usd, Side::Buy, None, qty("10"), true, None);
let blocks = report_blocks(&policy, &cancel);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, RejectCode::MissingRequiredField);
let h = holdings_of(&policy, acc, &usd).expect("must exist");
assert_eq!(h.held(), ps("2300"));
}
#[test]
fn sell_cancel_leftover_releases_underlying_held() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
None,
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd.clone(),
Side::Sell,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
None,
);
report_blocks(&policy, &fill);
let cancel = make_report(acc, aapl_usd, Side::Sell, None, qty("6"), true, None);
report_blocks(&policy, &cancel);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.held(), ps("0"));
assert_eq!(aapl.available(), ps("6"));
}
#[test]
fn final_report_with_zero_leaves_triggers_no_release() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let final_fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("10"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
report_blocks(&policy, &final_fill);
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("0"));
}
#[test]
fn buy_fill_creates_underlying_entry_in_holdings() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
assert!(holdings_of(&policy, acc, &asset("AAPL")).is_none());
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("1"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
report_blocks(&policy, &fill);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("entry must be created");
assert_eq!(aapl.available(), ps("1"));
}
#[test]
fn absolute_positive_sets_available() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Absolute(ps("15000"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("15000"));
}
#[test]
fn absolute_negative_sets_available() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Absolute(ps("-100"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("-100"));
}
#[test]
fn delta_positive_adds_to_available() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Delta(ps("5000"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("15000"));
}
#[test]
fn delta_negative_reduces_available() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Delta(ps("-3000"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("7000"));
}
#[test]
fn delta_below_zero_sets_negative_available() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Delta(ps("-15000"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("-5000"));
}
#[test]
fn delta_on_missing_creates_entry_from_zero() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = adj(asset("EUR"), Some(AdjustmentAmount::Delta(ps("100"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("EUR")).expect("entry must be created");
assert_eq!(h.available(), ps("100"));
assert_eq!(h.held(), ps("0"));
}
#[test]
fn lower_bound_exceeded_rejects() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = bounded_adj(
asset("USD"),
Some(AdjustmentAmount::Delta(ps("-15000"))),
Some(ps("0")),
None,
);
let mut mutations = Mutations::new();
let rejects = apply_adj(&policy, acc, &adjustment, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::AccountAdjustmentBoundsExceeded);
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("10000")); }
#[test]
fn upper_bound_exceeded_rejects() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = bounded_adj(
asset("USD"),
Some(AdjustmentAmount::Delta(ps("5000"))),
None,
Some(ps("12000")),
);
let mut mutations = Mutations::new();
let rejects = apply_adj(&policy, acc, &adjustment, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::AccountAdjustmentBoundsExceeded);
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("10000")); }
#[test]
fn absolute_creates_entry_for_new_asset() {
let acc = account(99224416);
let policy = build_policy(None, None);
assert!(holdings_of(&policy, acc, &asset("EUR")).is_none());
let adjustment = adj(asset("EUR"), Some(AdjustmentAmount::Absolute(ps("1000"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("EUR")).expect("entry must be created");
assert_eq!(h.available(), ps("1000"));
assert_eq!(h.held(), ps("0"));
}
#[test]
fn adjustment_rollback_restores_previous_state() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Absolute(ps("15000"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
let after_adj = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(after_adj.available(), ps("15000"));
mutations.rollback_all();
let after_rollback = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(after_rollback.available(), ps("10000"));
}
#[test]
fn adjustment_rollback_removes_newly_created_entry() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = adj(asset("EUR"), Some(AdjustmentAmount::Absolute(ps("1000"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
assert!(holdings_of(&policy, acc, &asset("EUR")).is_some());
mutations.rollback_all();
assert!(holdings_of(&policy, acc, &asset("EUR")).is_none());
}
#[test]
fn adjustment_rollback_restores_pruned_existing_entry() {
let acc = account(77112233);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "100");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Absolute(ps("0"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
assert!(holdings_of(&policy, acc, &asset("USD")).is_none());
mutations.rollback_all();
let after_rollback =
holdings_of(&policy, acc, &asset("USD")).expect("rollback must restore the pruned entry");
assert_eq!(after_rollback.available(), ps("100"));
}
#[test]
fn adjustment_rollback_restores_pruned_existing_entry_all_fields() {
let acc = account(55667788);
let policy = build_policy(None, None);
let setup = all_fields_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("100"))),
Some(AdjustmentAmount::Absolute(ps("20"))),
Some(AdjustmentAmount::Absolute(ps("5"))),
);
let mut setup_mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &setup, &mut setup_mutations).expect("seed must succeed");
setup_mutations.commit_all();
let zeroing = all_fields_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("0"))),
Some(AdjustmentAmount::Absolute(ps("0"))),
Some(AdjustmentAmount::Absolute(ps("0"))),
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &zeroing, &mut mutations).expect("must succeed");
assert!(
holdings_of(&policy, acc, &asset("USD")).is_none(),
"slot must be pruned after all-zero adjustment"
);
mutations.rollback_all();
let after_rollback =
holdings_of(&policy, acc, &asset("USD")).expect("rollback must restore the pruned entry");
assert_eq!(after_rollback.available(), ps("100"));
assert_eq!(after_rollback.held(), ps("20"));
assert_eq!(after_rollback.incoming(), ps("5"));
}
#[test]
fn adjustment_without_balance_asset_rejects_missing_required_field() {
struct NoBalanceAsset;
impl HasBalanceAsset for NoBalanceAsset {
fn balance_asset(&self) -> Result<&Asset, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("balance_asset"))
}
}
impl HasAccountAdjustmentBalance for NoBalanceAsset {
fn balance(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentBalanceAverageEntryPrice for NoBalanceAsset {
fn balance_average_entry_price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentBalanceRealizedPnl for NoBalanceAsset {
fn balance_realized_pnl(&self) -> Result<Option<Pnl>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentBalanceLowerBound for NoBalanceAsset {
fn balance_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentBalanceUpperBound for NoBalanceAsset {
fn balance_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentHeld for NoBalanceAsset {
fn held(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentHeldLowerBound for NoBalanceAsset {
fn held_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentHeldUpperBound for NoBalanceAsset {
fn held_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncoming for NoBalanceAsset {
fn incoming(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncomingLowerBound for NoBalanceAsset {
fn incoming_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncomingUpperBound for NoBalanceAsset {
fn incoming_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
let acc = account(99224416);
let policy = build_policy(None, None);
let mut mutations = Mutations::new();
let rejects = <TestPolicy as PreTradePolicy<
TestOrder,
TestReport,
NoBalanceAsset,
crate::core::FullSync,
>>::apply_account_adjustment(
&policy,
&AccountAdjustmentContext::new_test(dummy_control(acc)),
acc,
&NoBalanceAsset,
&mut mutations,
)
.expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::MissingRequiredField);
assert!(mutations.is_empty());
}
#[test]
fn adjustment_with_all_none_fields_returns_ok_without_changes() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = adj(asset("USD"), None); let mut mutations = Mutations::new();
let result = apply_adj(&policy, acc, &adjustment, &mut mutations);
assert!(result.is_ok());
assert!(mutations.is_empty());
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("5000")); }
#[test]
fn held_absolute_sets_held_directly() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = held_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("3000"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("3000"));
assert_eq!(h.available(), ps("10000")); assert_eq!(h.incoming(), ps("0"));
}
#[test]
fn held_delta_modifies_held() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let set = held_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("500"))),
None,
None,
);
apply_adj(&policy, acc, &set, &mut Mutations::with_capacity(1))
.expect("seed held must succeed");
let adjustment = held_adj(
asset("USD"),
Some(AdjustmentAmount::Delta(ps("200"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("700"));
}
#[test]
fn held_negative_value_is_allowed() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = held_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("-200"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.held(), ps("-200"));
}
#[test]
fn held_bounds_exceeded_rejects() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = held_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("1000"))),
None,
Some(ps("500")), );
let mut mutations = Mutations::new();
let rejects = apply_adj(&policy, acc, &adjustment, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::AccountAdjustmentBoundsExceeded);
assert!(mutations.is_empty());
}
#[test]
fn held_adjustment_returns_held_outcome_only() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = held_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("300"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
let entries = run_adjustment(&policy, acc, &adjustment, &mut mutations);
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert!(entry.balance.is_none(), "balance must be absent");
assert!(entry.incoming.is_none(), "incoming must be absent");
let held = entry.held.as_ref().expect("held outcome must be present");
assert_eq!(held.delta, ps("300")); assert_eq!(held.absolute, ps("300"));
}
#[test]
fn incoming_absolute_sets_incoming_directly() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let adjustment = incoming_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("2000"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.incoming(), ps("2000"));
assert_eq!(h.available(), ps("10000")); assert_eq!(h.held(), ps("0"));
}
#[test]
fn incoming_delta_modifies_incoming() {
let acc = account(99224416);
let policy = build_policy(None, None);
let set = incoming_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("1000"))),
None,
None,
);
apply_adj(&policy, acc, &set, &mut Mutations::with_capacity(1))
.expect("seed incoming must succeed");
let adjustment = incoming_adj(
asset("USD"),
Some(AdjustmentAmount::Delta(ps("-300"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.incoming(), ps("700"));
}
#[test]
fn incoming_negative_value_is_allowed() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = incoming_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("-500"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
mutations.commit_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.incoming(), ps("-500"));
}
#[test]
fn incoming_bounds_exceeded_rejects() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = incoming_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("200"))),
Some(ps("300")), None,
);
let mut mutations = Mutations::new();
let rejects = apply_adj(&policy, acc, &adjustment, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::AccountAdjustmentBoundsExceeded);
assert!(mutations.is_empty());
}
#[test]
fn incoming_adjustment_returns_incoming_outcome_only() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = incoming_adj(
asset("USD"),
Some(AdjustmentAmount::Delta(ps("400"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
let entries = run_adjustment(&policy, acc, &adjustment, &mut mutations);
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert!(entry.balance.is_none(), "balance must be absent");
assert!(entry.held.is_none(), "held must be absent");
let incoming = entry
.incoming
.as_ref()
.expect("incoming outcome must be present");
assert_eq!(incoming.delta, ps("400")); assert_eq!(incoming.absolute, ps("400"));
}
#[test]
fn all_three_fields_applied_and_reported() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = all_fields_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("8000"))),
Some(AdjustmentAmount::Absolute(ps("1500"))),
Some(AdjustmentAmount::Absolute(ps("600"))),
);
let mut mutations = Mutations::with_capacity(1);
let entries = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let balance = entry.balance.as_ref().expect("balance must be present");
assert_eq!(balance.absolute, ps("8000"));
assert_eq!(balance.delta, ps("3000"));
let held = entry.held.as_ref().expect("held must be present");
assert_eq!(held.absolute, ps("1500"));
assert_eq!(held.delta, ps("1500"));
let incoming = entry.incoming.as_ref().expect("incoming must be present");
assert_eq!(incoming.absolute, ps("600"));
assert_eq!(incoming.delta, ps("600"));
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(h.available(), ps("8000"));
assert_eq!(h.held(), ps("1500"));
assert_eq!(h.incoming(), ps("600"));
}
#[test]
fn all_three_rollback_restores_all_fields() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
apply_adj(
&policy,
acc,
&held_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("100"))),
None,
None,
),
&mut Mutations::with_capacity(1),
)
.expect("held seed must succeed");
apply_adj(
&policy,
acc,
&incoming_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("200"))),
None,
None,
),
&mut Mutations::with_capacity(1),
)
.expect("incoming seed must succeed");
let adjustment = all_fields_adj(
asset("USD"),
Some(AdjustmentAmount::Absolute(ps("9000"))),
Some(AdjustmentAmount::Absolute(ps("900"))),
Some(AdjustmentAmount::Absolute(ps("400"))),
);
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("must succeed");
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist after adjustment");
assert_eq!(h.available(), ps("9000"));
assert_eq!(h.held(), ps("900"));
assert_eq!(h.incoming(), ps("400"));
mutations.rollback_all();
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist after rollback");
assert_eq!(h.available(), ps("5000"));
assert_eq!(h.held(), ps("100"));
assert_eq!(h.incoming(), ps("200"));
}
fn run_pre_trade(
policy: &TestPolicy,
order: &TestOrder,
mutations: &mut Mutations,
) -> crate::pretrade::PolicyPreTradeResult {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::perform_pre_trade_check(
policy,
&PreTradeContext::new(None),
order,
mutations,
)
.expect("pre-trade must succeed")
.expect("spot funds policy must produce a result")
}
fn run_adjustment(
policy: &TestPolicy,
account_id: AccountId,
adjustment: &TestAdjustment,
mutations: &mut Mutations,
) -> Vec<crate::AccountOutcomeEntry> {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_account_adjustment(
policy,
&AccountAdjustmentContext::new_test(dummy_control(account_id)),
account_id,
adjustment,
mutations,
)
.expect("adjustment must succeed")
}
fn run_adjustment_result(
policy: &TestPolicy,
account_id: AccountId,
adjustment: &TestAdjustment,
mutations: &mut Mutations,
) -> Result<Vec<crate::AccountOutcomeEntry>, crate::pretrade::Rejects> {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_account_adjustment(
policy,
&AccountAdjustmentContext::new_test(dummy_control(account_id)),
account_id,
adjustment,
mutations,
)
}
fn run_report(policy: &TestPolicy, report: &TestReport) -> crate::pretrade::PostTradeResult {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_execution_report(
policy,
&post_trade_ctx(report),
report,
)
.expect("apply_execution_report must produce a result")
}
fn run_report_with_currency(
policy: &TestPolicy,
report: &TestReport,
currency: Asset,
) -> crate::pretrade::PostTradeResult {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_execution_report(
policy,
&crate::pretrade::PostTradeContext::with_account_currency(report.account_id, currency),
report,
)
.expect("apply_execution_report must produce a result")
}
fn run_report_without_account_currency(
policy: &TestPolicy,
report: &TestReport,
) -> crate::pretrade::PostTradeResult {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_execution_report(
policy,
&crate::pretrade::PostTradeContext::new(),
report,
)
.expect("apply_execution_report must produce a result")
}
#[test]
fn pre_trade_check_buy_returns_charge_outcome_and_lock_price() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
let outcome = run_pre_trade(&policy, &order, &mut mutations);
assert_eq!(outcome.account_adjustments.len(), 1);
let entry = &outcome.account_adjustments[0];
assert_eq!(entry.asset, asset("USD"));
let balance = entry
.balance
.as_ref()
.expect("balance delta must be present");
assert_eq!(balance.delta, ps("-2000"));
assert_eq!(balance.absolute, ps("8000"));
let held = entry.held.as_ref().expect("held delta must be present");
assert_eq!(held.delta, ps("2000"));
assert_eq!(held.absolute, ps("2000"));
assert!(entry.incoming.is_none());
assert_eq!(outcome.lock_prices.as_slice(), &[px("200")]);
}
#[test]
fn pre_trade_check_buy_market_lock_price_is_effective_price() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy_with_market_data(aapl_usd.clone(), px("100"), 1000);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("5")),
None,
);
let mut mutations = Mutations::with_capacity(1);
let outcome = run_pre_trade(&policy, &order, &mut mutations);
assert_eq!(outcome.lock_prices.as_slice(), &[px("110")]);
let entry = &outcome.account_adjustments[0];
assert_eq!(entry.asset, asset("USD"));
let held = entry.held.as_ref().expect("held delta must be present");
assert_eq!(held.delta, ps("550"));
}
#[test]
fn pre_trade_check_sell_returns_charge_outcome_and_no_lock_price() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "100");
let order = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Quantity(qty("3")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
let outcome = run_pre_trade(&policy, &order, &mut mutations);
assert_eq!(outcome.account_adjustments.len(), 1);
let entry = &outcome.account_adjustments[0];
assert_eq!(entry.asset, asset("AAPL"));
let held = entry.held.as_ref().expect("held delta must be present");
assert_eq!(held.delta, ps("3"));
assert_eq!(held.absolute, ps("3"));
assert!(outcome.lock_prices.is_empty());
}
#[test]
fn account_adjustment_returns_balance_delta_outcome_for_delta_amount() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Delta(ps("750"))));
let mut mutations = Mutations::with_capacity(1);
let entries = run_adjustment(&policy, acc, &adjustment, &mut mutations);
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.asset, asset("USD"));
let balance = entry
.balance
.as_ref()
.expect("balance delta must be present");
assert_eq!(balance.delta, ps("750"));
assert_eq!(balance.absolute, ps("5750"));
assert!(entry.held.is_none());
assert!(entry.incoming.is_none());
}
#[test]
fn account_adjustment_returns_balance_delta_outcome_for_absolute_amount() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Absolute(ps("8000"))));
let mut mutations = Mutations::with_capacity(1);
let entries = run_adjustment(&policy, acc, &adjustment, &mut mutations);
assert_eq!(entries.len(), 1);
let balance = entries[0]
.balance
.as_ref()
.expect("balance delta must be present");
assert_eq!(balance.delta, ps("3000"));
assert_eq!(balance.absolute, ps("8000"));
}
#[test]
fn account_adjustment_returns_zero_delta_entry_for_same_absolute_amount() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Absolute(ps("5000"))));
let mut mutations = Mutations::with_capacity(1);
let entries = run_adjustment(&policy, acc, &adjustment, &mut mutations);
assert_eq!(entries.len(), 1);
let balance = entries[0]
.balance
.as_ref()
.expect("balance delta must be present");
assert_eq!(balance.delta, ps("0"));
assert_eq!(balance.absolute, ps("5000"));
}
#[test]
fn account_adjustment_returns_empty_when_balance_field_is_none() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let adjustment = adj(asset("USD"), None);
let mut mutations = Mutations::new();
let entries = run_adjustment(&policy, acc, &adjustment, &mut mutations);
assert!(entries.is_empty());
}
#[test]
fn execution_report_buy_fill_returns_charge_and_counter_outcomes() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let result = run_report(&policy, &fill);
assert!(result.account_blocks.is_empty());
assert_eq!(result.account_adjustments.len(), 2);
let usd_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("USD"))
.expect("USD entry must exist");
assert!(usd_entry.entry.balance.is_none());
let usd_held = usd_entry
.entry
.held
.as_ref()
.expect("USD held delta must be present");
assert_eq!(usd_held.delta, ps("-800"));
assert_eq!(usd_held.absolute, ps("1200"));
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
let aapl_balance = aapl_entry
.entry
.balance
.as_ref()
.expect("AAPL balance delta must be present");
assert_eq!(aapl_balance.delta, ps("4"));
assert_eq!(aapl_balance.absolute, ps("4"));
assert!(aapl_entry.entry.held.is_none());
}
#[test]
fn execution_report_buy_final_with_fill_and_release_merges_charge_outcome() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let final_report = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let result = run_report(&policy, &final_report);
let usd_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("USD"))
.expect("USD entry must exist");
let usd_held = usd_entry
.entry
.held
.as_ref()
.expect("USD held delta must be present");
assert_eq!(usd_held.delta, ps("-2000"));
assert_eq!(usd_held.absolute, ps("0"));
let usd_balance = usd_entry
.entry
.balance
.as_ref()
.expect("USD balance delta must be present");
assert_eq!(usd_balance.delta, ps("1200"));
assert_eq!(usd_balance.absolute, ps("9200"));
}
#[test]
fn execution_report_buy_release_with_missing_lock_price_emits_block() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let cancel = make_report(acc, aapl_usd, Side::Buy, None, qty("10"), true, None);
let result = run_report(&policy, &cancel);
assert_eq!(result.account_blocks.len(), 1);
assert_eq!(
result.account_blocks[0].code,
RejectCode::MissingRequiredField
);
assert!(result.account_adjustments.is_empty());
}
#[test]
fn execution_report_buy_release_with_multiple_lock_prices_emits_block() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let mut lock = PreTradeLock::new();
lock.push(DEFAULT_POLICY_GROUP_ID, px("200"));
lock.push(DEFAULT_POLICY_GROUP_ID, px("210"));
let cancel = make_report(acc, aapl_usd, Side::Buy, None, qty("10"), true, Some(lock));
let result = run_report(&policy, &cancel);
assert_eq!(result.account_blocks.len(), 1);
assert_eq!(result.account_blocks[0].code, RejectCode::Other);
}
#[test]
fn execution_report_sell_final_release_does_not_consult_lock() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "100");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let cancel = make_report(acc, aapl_usd, Side::Sell, None, qty("10"), true, None);
let result = run_report(&policy, &cancel);
assert!(result.account_blocks.is_empty());
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
let held = aapl_entry
.entry
.held
.as_ref()
.expect("AAPL held delta must be present");
assert_eq!(held.delta, ps("-10"));
assert_eq!(held.absolute, ps("0"));
let balance = aapl_entry
.entry
.balance
.as_ref()
.expect("AAPL balance delta must be present");
assert_eq!(balance.delta, ps("10"));
assert_eq!(balance.absolute, ps("100"));
}
fn position_size_max() -> PositionSize {
PositionSize::new(rust_decimal::Decimal::MAX)
}
#[test]
fn fill_consume_exceeds_held_drives_held_negative_without_blocking_account() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let usd = asset("USD");
policy
.holdings
.with_mut((acc, usd.clone()), Holdings::zero, |slot, _| {
*slot = Holdings::new(ps("0"), ps("100"));
});
let lock = PreTradeLock::from_entries([(DEFAULT_POLICY_GROUP_ID, px("200"))]);
let report = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("10"),
}),
qty("0"),
true,
Some(lock),
);
let blocks = report_blocks(&policy, &report);
assert!(
blocks.is_empty(),
"venue-truth must not raise account block"
);
let h = holdings_of(&policy, acc, &usd).expect("must exist");
assert_eq!(h.held(), ps("-1900"));
assert_eq!(h.available(), ps("0"));
}
#[test]
fn fill_inflow_overflow_blocks_account() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let aapl = asset("AAPL");
policy
.holdings
.with_mut((acc, aapl.clone()), Holdings::zero, |slot, _| {
*slot = Holdings::new(position_size_max(), PositionSize::ZERO);
});
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("1")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("1"),
quantity: qty("1"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("1"),
)])),
);
let result = <TestPolicy as PreTradePolicy<
TestOrder,
TestReport,
TestAdjustment,
crate::core::FullSync,
>>::apply_execution_report(&policy, &post_trade_ctx(&fill), &fill)
.expect("must report a result");
assert_eq!(result.account_blocks.len(), 1);
assert_eq!(
result.account_blocks[0].code,
RejectCode::ArithmeticOverflow
);
let usd_adjustment = result
.account_adjustments
.iter()
.find(|a| a.entry.asset == asset("USD"))
.expect("outflow-side USD adjustment must be reported");
let held = usd_adjustment
.entry
.held
.as_ref()
.expect("USD held outcome must be present after partial outflow");
assert_eq!(held.absolute, ps("0"));
assert_eq!(held.delta, ps("-1"));
}
#[test]
fn fill_inflow_overflow_round_trip_blocks_account_in_engine() {
let acc = account(99224418);
let aapl_usd = instr("AAPL", "USD");
let engine = build_engine_with_spot_funds_policy();
let aapl = asset("AAPL");
seed_balance_via_engine(&engine, acc, aapl.clone(), position_size_max());
seed_balance_via_engine(&engine, acc, asset("USD"), ps("10000"));
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("1")),
);
let request = engine
.start_pre_trade(order)
.expect("start_pre_trade must succeed");
request.execute().expect("execute must reserve").commit();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("1"),
quantity: qty("1"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("1"),
)])),
);
let result = engine.apply_execution_report(&fill);
assert_eq!(result.account_blocks.len(), 1);
assert_eq!(
result.account_blocks[0].code,
RejectCode::ArithmeticOverflow
);
assert_account_blocked_with_arithmetic_overflow(&engine, acc);
}
#[test]
fn pre_trade_hold_overflow_rejects_with_arithmetic_overflow_code() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let usd = asset("USD");
policy
.holdings
.with_mut((acc, usd.clone()), Holdings::zero, |slot, _| {
*slot = Holdings::new(position_size_max(), position_size_max());
});
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("1")),
);
let mut mutations = Mutations::new();
let rejects = pre_trade_check(&policy, &order, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::ArithmeticOverflow);
assert!(mutations.is_empty());
}
#[test]
fn account_adjustment_delta_overflow_rejects_with_arithmetic_overflow_code() {
let acc = account(99224416);
let policy = build_policy(None, None);
let usd = asset("USD");
policy
.holdings
.with_mut((acc, usd.clone()), Holdings::zero, |slot, _| {
*slot = Holdings::new(position_size_max(), PositionSize::ZERO);
});
let adjustment = adj(
asset("USD"),
Some(AdjustmentAmount::Delta(position_size_max())),
);
let mut mutations = Mutations::new();
let rejects = apply_adj(&policy, acc, &adjustment, &mut mutations).expect_err("must reject");
assert_eq!(rejects[0].code, RejectCode::ArithmeticOverflow);
let h = holdings_of(&policy, acc, &usd).expect("must exist");
assert_eq!(h.available(), position_size_max());
}
#[test]
fn buy_fill_missing_charge_slot_records_negative_held_and_credits_counter() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let report = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("10"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let blocks = report_blocks(&policy, &report);
assert!(blocks.is_empty());
let usd = holdings_of(&policy, acc, &asset("USD")).expect("USD slot must be created");
assert_eq!(usd.available(), ps("0"));
assert_eq!(usd.held(), ps("-2000"));
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("AAPL slot must be created");
assert_eq!(aapl.available(), ps("10"));
assert_eq!(aapl.held(), ps("0"));
}
#[test]
fn sell_fill_missing_charge_slot_records_negative_held_and_credits_counter() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let report = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("200"),
quantity: qty("10"),
}),
qty("0"),
true,
None,
);
let blocks = report_blocks(&policy, &report);
assert!(blocks.is_empty());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("AAPL slot must be created");
assert_eq!(aapl.available(), ps("0"));
assert_eq!(aapl.held(), ps("-10"));
let usd = holdings_of(&policy, acc, &asset("USD")).expect("USD slot must be created");
assert_eq!(usd.available(), ps("2000"));
assert_eq!(usd.held(), ps("0"));
}
#[test]
fn cancel_release_missing_charge_slot_applies_release_delta() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let report = make_report(
acc,
aapl_usd,
Side::Buy,
None,
qty("10"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let blocks = report_blocks(&policy, &report);
assert!(blocks.is_empty());
let usd = holdings_of(&policy, acc, &asset("USD")).expect("USD slot must be created");
assert_eq!(usd.available(), ps("2000"));
assert_eq!(usd.held(), ps("-2000"));
}
#[test]
fn zero_adjustment_on_missing_slot_does_not_create_entry() {
let acc = account(99224416);
let policy = build_policy(None, None);
for amount in [
AdjustmentAmount::Absolute(ps("0")),
AdjustmentAmount::Delta(ps("0")),
] {
let adjustment = adj(asset("EUR"), Some(amount));
let mut mutations = Mutations::new();
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("zero adjustment must succeed");
assert!(
holdings_of(&policy, acc, &asset("EUR")).is_none(),
"phantom entry must not be created for {amount:?}"
);
}
}
#[test]
fn slot_removed_when_adjustment_brings_all_fields_to_zero() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
assert!(
holdings_of(&policy, acc, &asset("USD")).is_some(),
"slot must exist after seed"
);
let adjustment = adj(asset("USD"), Some(AdjustmentAmount::Absolute(ps("0"))));
let mut mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &adjustment, &mut mutations).expect("adjustment must succeed");
assert!(
holdings_of(&policy, acc, &asset("USD")).is_none(),
"slot must be removed when adjustment drives it to zero"
);
}
#[test]
fn slot_removed_when_fill_outflow_brings_all_fields_to_zero() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "5000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("25")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let report = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("25"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let blocks = report_blocks(&policy, &report);
assert!(blocks.is_empty());
assert!(
holdings_of(&policy, acc, &asset("USD")).is_none(),
"USD slot must be pruned when fill drives it to (0, 0)"
);
}
#[test]
fn buy_qty_zero_pre_trade_check_does_not_create_phantom_slot() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("0")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("zero-qty hold must succeed");
mutations.commit_all();
assert!(
holdings_of(&policy, acc, &asset("USD")).is_none(),
"no phantom USD slot must remain after zero-charge hold",
);
}
#[test]
fn buy_volume_zero_pre_trade_check_does_not_create_phantom_slot() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Volume(vol("0")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("zero-volume hold must succeed");
mutations.commit_all();
assert!(
holdings_of(&policy, acc, &asset("USD")).is_none(),
"no phantom USD slot must remain after zero-charge hold",
);
}
#[test]
fn hold_rollback_restores_pruned_existing_entry() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let usd = asset("USD");
seed(&policy, acc, usd.clone(), "200");
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("200")),
);
let mut hold_mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut hold_mutations).expect("hold must succeed");
assert_eq!(
holdings_of(&policy, acc, &usd).expect("slot must exist after hold"),
Holdings::new(ps("0"), ps("200")),
);
let zeroing = all_fields_adj(
usd.clone(),
Some(AdjustmentAmount::Absolute(ps("0"))),
Some(AdjustmentAmount::Absolute(ps("0"))),
None,
);
let mut adj_mutations = Mutations::with_capacity(1);
apply_adj(&policy, acc, &zeroing, &mut adj_mutations).expect("zeroing must succeed");
adj_mutations.commit_all();
assert!(
holdings_of(&policy, acc, &usd).is_none(),
"slot must be pruned after zero adjustment",
);
hold_mutations.rollback_all();
let restored = holdings_of(&policy, acc, &usd).expect("rollback must recreate the pruned slot");
assert_eq!(restored.available(), ps("200"));
assert_eq!(restored.held(), ps("-200"));
}
#[test]
fn buy_fill_lock_savings_subtraction_at_decimal_extremes_does_not_panic() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let max_qty = Quantity::new_unchecked(rust_decimal::Decimal::MAX);
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("1"),
quantity: max_qty,
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("0"),
)])),
);
let _ = report_blocks(&policy, &fill);
}
fn build_engine_with_spot_funds_policy(
) -> crate::FullSyncEngine<TestOrder, TestReport, TestAdjustment> {
let builder = crate::Engine::builder::<TestOrder, TestReport, TestAdjustment>().full_sync();
let policy: SpotFundsPolicy<FullSync, FullSync> =
SpotFundsPolicy::new(settings(0), None, builder.storage_builder());
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
fn seed_balance_via_engine(
engine: &crate::FullSyncEngine<TestOrder, TestReport, TestAdjustment>,
account_id: AccountId,
seeded_asset: Asset,
amount: PositionSize,
) {
let adjustment = TestAdjustment {
asset: seeded_asset,
balance: Some(AdjustmentAmount::Absolute(amount)),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
engine
.apply_account_adjustment(account_id, &[adjustment])
.expect("seed adjustment must succeed");
}
fn assert_account_blocked_with_arithmetic_overflow(
engine: &crate::FullSyncEngine<TestOrder, TestReport, TestAdjustment>,
account_id: AccountId,
) {
let probe = make_order(
account_id,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Quantity(qty("0")),
Some(px("1")),
);
let rejects = engine
.start_pre_trade(probe)
.expect_err("account must be blocked");
assert!(
rejects
.iter()
.any(|r| r.code == RejectCode::ArithmeticOverflow),
"blocked-account reject must carry ArithmeticOverflow: {rejects:?}",
);
}
#[test]
fn hold_rollback_overflow_blocks_account_via_engine() {
use rust_decimal::Decimal;
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let engine = build_engine_with_spot_funds_policy();
let aapl = asset("AAPL");
let max_minus_fifty = PositionSize::new(Decimal::MAX - rust_decimal::Decimal::from(50));
seed_balance_via_engine(&engine, acc, aapl.clone(), max_minus_fifty);
let order = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Quantity(qty("50")),
Some(px("1")),
);
let request = engine
.start_pre_trade(order)
.expect("start_pre_trade must succeed");
let reservation = request.execute().expect("execute must reserve");
let bump = TestAdjustment {
asset: aapl.clone(),
balance: Some(AdjustmentAmount::Absolute(PositionSize::new(Decimal::MAX))),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
engine
.apply_account_adjustment(acc, &[bump])
.expect("bump must succeed");
drop(reservation);
assert_account_blocked_with_arithmetic_overflow(&engine, acc);
}
#[test]
fn adjustment_rollback_overflow_blocks_account_via_engine() {
use rust_decimal::Decimal;
let acc = account(99224417);
let engine = build_engine_with_spot_funds_policy();
let usd = asset("USD");
seed_balance_via_engine(&engine, acc, usd.clone(), ps("1000"));
let element_one = TestAdjustment {
asset: usd.clone(),
balance: Some(AdjustmentAmount::Delta(ps("10"))),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let element_two_fails = TestAdjustment {
asset: usd.clone(),
balance: Some(AdjustmentAmount::Delta(ps("1"))),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: Some(PositionSize::new(Decimal::from(5))),
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let outcome = engine.apply_account_adjustment(acc, &[element_one, element_two_fails]);
assert!(outcome.is_err(), "batch with violating element must reject");
let probe = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Quantity(qty("0")),
Some(px("1")),
);
let probe_outcome = engine.start_pre_trade(probe);
assert!(
probe_outcome.is_ok(),
"successful rollback must not block the account",
);
}
#[test]
fn hold_rollback_overflow_blocks_account_via_local_engine() {
use rust_decimal::Decimal;
let acc = account(99224418);
let aapl_usd = instr("AAPL", "USD");
let aapl = asset("AAPL");
let builder = crate::Engine::builder::<TestOrder, TestReport, TestAdjustment>().no_sync();
let policy: SpotFundsPolicy<crate::LocalSync, crate::LocalSync> =
SpotFundsPolicy::new(settings(0), None, builder.storage_builder());
let engine: crate::LocalEngine<TestOrder, TestReport, TestAdjustment> = builder
.pre_trade(policy)
.build()
.expect("engine must build");
let max_minus_fifty = PositionSize::new(Decimal::MAX - rust_decimal::Decimal::from(50));
let seed = TestAdjustment {
asset: aapl.clone(),
balance: Some(AdjustmentAmount::Absolute(max_minus_fifty)),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
engine
.apply_account_adjustment(acc, &[seed])
.expect("seed adjustment must succeed");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("50")),
Some(px("1")),
);
let request = engine
.start_pre_trade(order)
.expect("start_pre_trade must succeed");
let reservation = request.execute().expect("execute must reserve");
let bump = TestAdjustment {
asset: aapl.clone(),
balance: Some(AdjustmentAmount::Absolute(PositionSize::new(Decimal::MAX))),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
engine
.apply_account_adjustment(acc, &[bump])
.expect("bump must succeed");
drop(reservation);
let probe = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Quantity(qty("0")),
Some(px("1")),
);
let rejects = engine
.start_pre_trade(probe)
.expect_err("account must be blocked");
assert!(
rejects
.iter()
.any(|r| r.code == RejectCode::ArithmeticOverflow),
"blocked-account reject must carry ArithmeticOverflow: {rejects:?}",
);
}
#[test]
fn spot_funds_account_sync_is_send() {
fn assert_send<T: Send>() {}
assert_send::<SpotFundsPolicy<crate::AccountSync, FullSync>>();
}
#[test]
fn hold_rollback_overflow_blocks_account_with_account_sync_storage() {
use crate::core::account_control::BlockedAccounts;
use crate::core::{AccountBlockHandle, AccountControl};
use crate::storage::{IndexLocking, LockingPolicyFactory, StorageBuilder};
use crate::AccountKeyConstraint;
use rust_decimal::Decimal;
type AccountSyncFactory = IndexLocking<AccountKeyConstraint>;
type AccountSyncPolicy = SpotFundsPolicy<crate::AccountSync, FullSync>;
type Policy = dyn PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::AccountSync>;
let acc = account(99224419);
let aapl_usd = instr("AAPL", "USD");
let aapl = asset("AAPL");
let factory = IndexLocking::<AccountKeyConstraint>::default();
let storage_builder = StorageBuilder::new(factory);
let blocked = <AccountSyncFactory as LockingPolicyFactory>::new_shared(BlockedAccounts::new(
&storage_builder,
));
let groups = crate::core::account_groups::AccountGroups::new(&storage_builder);
let policy: AccountSyncPolicy = SpotFundsPolicy::new(settings(0), None, &storage_builder);
let make_control = || AccountControl::new(AccountBlockHandle::from_inner(blocked.clone()), acc);
let mut seed_mutations = Mutations::new();
let max_minus_fifty = PositionSize::new(Decimal::MAX - rust_decimal::Decimal::from(50));
let seed = TestAdjustment {
asset: aapl.clone(),
balance: Some(AdjustmentAmount::Absolute(max_minus_fifty)),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
<Policy>::apply_account_adjustment(
&policy,
&AccountAdjustmentContext::new_test(make_control()),
acc,
&seed,
&mut seed_mutations,
)
.expect("seed adjustment must succeed");
seed_mutations.commit_all();
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("50")),
Some(px("1")),
);
let mut hold_mutations = Mutations::new();
<Policy>::perform_pre_trade_check(
&policy,
&PreTradeContext::new(Some(make_control())),
&order,
&mut hold_mutations,
)
.expect("pre-trade check must reserve");
let mut bump_mutations = Mutations::new();
let bump = TestAdjustment {
asset: aapl.clone(),
balance: Some(AdjustmentAmount::Absolute(PositionSize::new(Decimal::MAX))),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
<Policy>::apply_account_adjustment(
&policy,
&AccountAdjustmentContext::new_test(make_control()),
acc,
&bump,
&mut bump_mutations,
)
.expect("bump adjustment must succeed");
bump_mutations.commit_all();
hold_mutations.rollback_all();
let probe = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Quantity(qty("0")),
Some(px("1")),
);
let rejects = blocked
.check(&groups, &probe, crate::pretrade::RejectScope::Order)
.expect("account must be blocked");
assert!(
rejects
.iter()
.any(|r| r.code == RejectCode::ArithmeticOverflow),
"blocked-account reject must carry ArithmeticOverflow: {rejects:?}",
);
}
fn pre_trade_full(
policy: &TestPolicy,
order: &TestOrder,
mutations: &mut Mutations,
) -> Result<crate::pretrade::PolicyPreTradeResult, crate::pretrade::Rejects> {
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::perform_pre_trade_check(
policy,
&PreTradeContext::new(None),
order,
mutations,
)
.map(|opt| opt.expect("pre-trade must produce a result"))
}
fn maybe_holdings(policy: &TestPolicy, acc: AccountId, asset_code: &str) -> Option<Holdings> {
holdings_of(policy, acc, &asset(asset_code))
}
fn assert_balance(policy: &TestPolicy, acc: AccountId, asset_code: &str, avail: &str, held: &str) {
let h = maybe_holdings(policy, acc, asset_code).unwrap_or_else(Holdings::zero);
assert_eq!(
h.available(),
ps(avail),
"{asset_code} available mismatch (held {})",
h.held()
);
assert_eq!(
h.held(),
ps(held),
"{asset_code} held mismatch (available {})",
h.available()
);
}
#[test]
fn buy_qty_zero_price_reserves_nothing_and_settles() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("0")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert!(
result.account_adjustments.is_empty(),
"zero-reservation buy emits no leg adjustments",
);
assert_eq!(result.lock_prices.as_slice(), &[px("0")]);
assert!(maybe_holdings(&policy, acc, "USD").is_none());
let partial = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("0"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("0"),
)])),
);
assert!(report_blocks(&policy, &partial).is_empty());
assert_balance(&policy, acc, "AAPL", "4", "0");
assert!(maybe_holdings(&policy, acc, "USD").is_none());
let final_fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("0"),
quantity: qty("6"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("0"),
)])),
);
assert!(report_blocks(&policy, &final_fill).is_empty());
assert_balance(&policy, acc, "AAPL", "10", "0");
assert!(maybe_holdings(&policy, acc, "USD").is_none());
}
#[test]
fn buy_qty_zero_price_cancel_releases_nothing() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("0")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
let cancel = make_report(
acc,
aapl_usd,
Side::Buy,
None,
qty("10"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("0"),
)])),
);
assert!(report_blocks(&policy, &cancel).is_empty());
assert!(maybe_holdings(&policy, acc, "USD").is_none());
}
#[test]
fn buy_qty_negative_price_reserves_nothing_and_receives_cash_on_fill() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("-50")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert!(result.account_adjustments.is_empty());
assert_eq!(result.lock_prices.as_slice(), &[px("-50")]);
assert!(maybe_holdings(&policy, acc, "USD").is_none());
let partial = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("-50"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
assert!(report_blocks(&policy, &partial).is_empty());
assert_balance(&policy, acc, "AAPL", "4", "0");
assert_balance(&policy, acc, "USD", "200", "0");
let final_fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("-50"),
quantity: qty("6"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
assert!(report_blocks(&policy, &final_fill).is_empty());
assert_balance(&policy, acc, "AAPL", "10", "0");
assert_balance(&policy, acc, "USD", "500", "0");
}
#[test]
fn buy_qty_negative_price_cancel_releases_nothing() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("-50")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
let partial = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("-50"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
report_blocks(&policy, &partial);
let cancel = make_report(
acc,
aapl_usd,
Side::Buy,
None,
qty("6"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
assert!(report_blocks(&policy, &cancel).is_empty());
assert_balance(&policy, acc, "AAPL", "4", "0");
assert_balance(&policy, acc, "USD", "200", "0");
}
#[test]
fn buy_volume_zero_price_reserves_nothing() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Volume(vol("2000")),
Some(px("0")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert!(
result.account_adjustments.is_empty(),
"zero-price volume buy reserves no settlement (no stuck held)",
);
assert!(maybe_holdings(&policy, acc, "USD").is_none());
}
#[test]
fn buy_volume_negative_price_reserves_nothing() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Volume(vol("2000")),
Some(px("-50")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert!(result.account_adjustments.is_empty());
assert!(maybe_holdings(&policy, acc, "USD").is_none());
}
#[test]
fn sell_qty_zero_price_reserves_only_underlying() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("0")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert_eq!(result.account_adjustments.len(), 1);
assert!(result.lock_prices.is_empty());
assert_balance(&policy, acc, "AAPL", "0", "10");
let partial = make_report(
acc,
aapl_usd.clone(),
Side::Sell,
Some(Trade {
price: px("0"),
quantity: qty("4"),
}),
qty("6"),
false,
None,
);
assert!(report_blocks(&policy, &partial).is_empty());
assert_balance(&policy, acc, "AAPL", "0", "6");
assert!(maybe_holdings(&policy, acc, "USD").is_none());
let final_fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("0"),
quantity: qty("6"),
}),
qty("0"),
true,
None,
);
assert!(report_blocks(&policy, &final_fill).is_empty());
assert_balance(&policy, acc, "AAPL", "0", "0");
}
#[test]
fn sell_qty_zero_price_cancel_releases_underlying() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("0")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
let cancel = make_report(acc, aapl_usd, Side::Sell, None, qty("10"), true, None);
assert!(report_blocks(&policy, &cancel).is_empty());
assert_balance(&policy, acc, "AAPL", "10", "0");
}
#[test]
fn sell_qty_negative_price_reserves_both_legs_and_settles() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
seed(&policy, acc, asset("USD"), "1000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("-50")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert_eq!(
result.account_adjustments.len(),
2,
"sell at negative price reserves both underlying and settlement legs",
);
assert_eq!(result.lock_prices.as_slice(), &[px("-50")]);
assert_balance(&policy, acc, "AAPL", "0", "10");
assert_balance(&policy, acc, "USD", "500", "500");
let partial = make_report(
acc,
aapl_usd.clone(),
Side::Sell,
Some(Trade {
price: px("-50"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
assert!(report_blocks(&policy, &partial).is_empty());
assert_balance(&policy, acc, "AAPL", "0", "6");
assert_balance(&policy, acc, "USD", "500", "300");
let final_fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("-50"),
quantity: qty("6"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
assert!(report_blocks(&policy, &final_fill).is_empty());
assert_balance(&policy, acc, "AAPL", "0", "0");
assert_balance(&policy, acc, "USD", "500", "0");
}
#[test]
fn sell_qty_negative_price_cancel_releases_both_legs() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
seed(&policy, acc, asset("USD"), "1000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("-50")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
let partial = make_report(
acc,
aapl_usd.clone(),
Side::Sell,
Some(Trade {
price: px("-50"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
report_blocks(&policy, &partial);
let cancel = make_report(
acc,
aapl_usd,
Side::Sell,
None,
qty("6"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
assert!(report_blocks(&policy, &cancel).is_empty());
assert_balance(&policy, acc, "AAPL", "6", "0");
assert_balance(&policy, acc, "USD", "800", "0");
}
#[test]
fn sell_volume_negative_price_reserves_both_legs() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "40");
seed(&policy, acc, asset("USD"), "5000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Volume(vol("2000")),
Some(px("-50")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert_eq!(result.account_adjustments.len(), 2);
assert_eq!(result.lock_prices.as_slice(), &[px("-50")]);
assert_balance(&policy, acc, "AAPL", "0", "40");
assert_balance(&policy, acc, "USD", "3000", "2000");
let final_fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("-50"),
quantity: qty("40"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("-50"),
)])),
);
assert!(report_blocks(&policy, &final_fill).is_empty());
assert_balance(&policy, acc, "AAPL", "0", "0");
assert_balance(&policy, acc, "USD", "3000", "0");
}
#[test]
fn sell_volume_zero_price_calc_failure_not_sign_reject() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "100");
let order = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Volume(vol("1000")),
Some(px("0")),
);
let mut mutations = Mutations::with_capacity(1);
let err = pre_trade_check(&policy, &order, &mut mutations).unwrap_err();
assert!(
err.iter()
.any(|r| r.code == RejectCode::OrderValueCalculationFailed),
"zero-price volume sell is a sizing calc failure: {err:?}",
);
assert!(mutations.is_empty());
assert_balance(&policy, acc, "AAPL", "100", "0");
}
#[test]
fn buy_qty_positive_price_held_returns_to_zero_after_full_settlement() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert_balance(&policy, acc, "USD", "8000", "2000");
let partial = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
report_blocks(&policy, &partial);
assert_balance(&policy, acc, "USD", "8000", "1200");
let final_fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("6"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
report_blocks(&policy, &final_fill);
assert_balance(&policy, acc, "USD", "8000", "0");
assert_balance(&policy, acc, "AAPL", "10", "0");
}
#[test]
fn sell_volume_positive_price_reserves_only_underlying_and_settles() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Volume(vol("2000")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
let result = pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
mutations.commit_all();
assert_eq!(result.account_adjustments.len(), 1);
assert!(result.lock_prices.is_empty());
assert_balance(&policy, acc, "AAPL", "0", "10");
let final_fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("200"),
quantity: qty("10"),
}),
qty("0"),
true,
None,
);
assert!(report_blocks(&policy, &final_fill).is_empty());
assert_balance(&policy, acc, "AAPL", "0", "0");
assert_balance(&policy, acc, "USD", "2000", "0");
}
#[test]
fn sell_negative_price_rollback_restores_both_legs() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
seed(&policy, acc, asset("USD"), "1000");
let order = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("-50")),
);
let mut mutations = Mutations::with_capacity(2);
pre_trade_full(&policy, &order, &mut mutations).expect("must pass");
assert_balance(&policy, acc, "AAPL", "0", "10");
assert_balance(&policy, acc, "USD", "500", "500");
mutations.rollback_all();
assert_balance(&policy, acc, "AAPL", "10", "0");
assert_balance(&policy, acc, "USD", "1000", "0");
}
#[test]
fn sell_negative_price_settlement_insufficient_rolls_back_underlying_leg() {
let acc = account(99224418);
let aapl_usd = instr("AAPL", "USD");
let engine = build_engine_with_spot_funds_policy();
seed_balance_via_engine(&engine, acc, asset("AAPL"), ps("10"));
seed_balance_via_engine(&engine, acc, asset("USD"), ps("100"));
let order = make_order(
acc,
aapl_usd,
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("-50")),
);
let rejects = match engine.execute_pre_trade(order) {
Ok(_) => panic!("settlement leg must reject for insufficient funds"),
Err(rejects) => rejects,
};
assert!(
rejects
.iter()
.any(|r| r.code == RejectCode::InsufficientFunds),
"settlement leg must reject with InsufficientFunds: {rejects:?}",
);
let probe = make_order(
acc,
instr("AAPL", "USD"),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut reservation = engine
.execute_pre_trade(probe)
.expect("a positive-price sell of the full 10 AAPL must still fit");
reservation.rollback();
}
#[test]
fn buy_fill_zero_price_nonzero_qty_consumes_held_by_lock_price() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("2")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("0"),
quantity: qty("2"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let result = run_report(&policy, &fill);
assert!(result.account_blocks.is_empty());
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert_eq!(aapl_entry.entry.average_entry_price, Some(px("0")));
assert!(aapl_entry.entry.realized_pnl.is_none());
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("0"), "held must be fully consumed");
assert_eq!(
usd.available(),
ps("10000"),
"full amount returned as savings"
);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("AAPL credited");
assert_eq!(aapl.available(), ps("2"));
assert_eq!(aapl.avg_entry_price(), Some(px("0")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn buy_fill_negative_trade_price_uses_signed_not_abs() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("2")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("-50"),
quantity: qty("2"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let result = run_report(&policy, &fill);
assert!(result.account_blocks.is_empty());
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert_eq!(aapl_entry.entry.average_entry_price, Some(px("-50")));
assert!(aapl_entry.entry.realized_pnl.is_none());
let usd = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(usd.held(), ps("0"));
assert_eq!(
usd.available(),
ps("10100"),
"signed savings = lock(200) - notional(-100) = 300 credited to available",
);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("AAPL credited");
assert_eq!(aapl.avg_entry_price(), Some(px("-50")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
fn ctx_with_group(
account_id: AccountId,
group_id: crate::param::AccountGroupId,
) -> crate::pretrade::PreTradeContext<crate::storage::FullLocking> {
use crate::core::{AccountGroups, AccountGroupsHandle};
use crate::storage::{FullLocking, LockingPolicyFactory, StorageBuilder};
let sb = StorageBuilder::new(FullLocking);
let groups = AccountGroups::new(&sb);
groups
.register_group(&[account_id], group_id)
.expect("registration must succeed");
let handle = AccountGroupsHandle::from_inner(FullLocking::new_shared(groups));
crate::pretrade::PreTradeContext::with_groups(None, handle, Some(account_id))
}
#[test]
fn buy_market_group_override_reserves_group_slippage_not_global() {
let acc = account(99224416);
let grp = crate::param::AccountGroupId::from_u32(5).expect("valid group id");
let aapl_usd = instr("AAPL", "USD");
let b = engine_builder();
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let id = svc
.register(aapl_usd.clone())
.expect("register must succeed");
svc.push(id, Quote::new().with_mark(px("100")))
.expect("push must succeed");
let overrides = [
(
SpotFundsOverrideTarget::InstrumentAccount(id, account(9999)),
SpotFundsOverride {
slippage_bps: Some(5000),
},
),
(
SpotFundsOverrideTarget::InstrumentAccountGroup(id, grp),
SpotFundsOverride {
slippage_bps: Some(2000),
},
),
];
let settings = SpotFundsSettings::new(
0, SpotFundsPricingSource::Mark,
overrides,
)
.expect("settings must build");
let bundle = SpotFundsMarketData::new(Arc::clone(&svc));
let policy = SpotFundsPolicy::new(settings, Some(bundle), b.storage_builder());
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd,
Side::Buy,
TradeAmount::Quantity(qty("10")),
None, );
let mut mutations = Mutations::with_capacity(1);
let ctx = ctx_with_group(acc, grp);
<TestPolicy as crate::pretrade::PreTradePolicy<
TestOrder,
TestReport,
TestAdjustment,
crate::core::FullSync,
>>::perform_pre_trade_check(&policy, &ctx, &order, &mut mutations)
.expect("must succeed");
let h = holdings_of(&policy, acc, &asset("USD")).expect("must exist");
assert_eq!(
h.held(),
ps("1200"),
"group override (2000 bps) must be used, not global (0 bps)",
);
assert_eq!(h.available(), ps("8800"));
}
fn seed_with_avg(
policy: &TestPolicy,
account_id: AccountId,
asset: Asset,
amount: &str,
avg: Price,
) {
let adjustment = TestAdjustment {
asset,
balance: Some(AdjustmentAmount::Absolute(ps(amount))),
balance_average_entry_price: Some(avg),
balance_realized_pnl: Some(Pnl::ZERO),
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let mut mutations = Mutations::with_capacity(1);
<TestPolicy as PreTradePolicy<TestOrder, TestReport, TestAdjustment, crate::core::FullSync>>::apply_account_adjustment(
policy,
&AccountAdjustmentContext::new_test(dummy_control(account_id)),
account_id,
&adjustment,
&mut mutations,
)
.expect("seed must succeed");
mutations.commit_all();
}
#[test]
fn balance_adjustment_with_avg_sets_slot_average_and_emits_it() {
let acc = account(99224416);
let policy = build_policy(None, None);
let adjustment = adj_with_avg(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(px("150")),
);
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(outcome.len(), 1);
let entry = &outcome[0];
assert_eq!(entry.average_entry_price, Some(px("150")));
assert!(entry.realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("150")));
assert_eq!(aapl.realized_pnl(), None);
}
#[test]
fn metadata_only_average_adjustment_sets_slot_average_and_emits_it() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed(&policy, acc, asset("AAPL"), "10");
let adjustment = adj_with_avg(asset("AAPL"), None, Some(px("150")));
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(outcome.len(), 1);
assert!(outcome[0].balance.is_none());
assert_eq!(outcome[0].average_entry_price, Some(px("150")));
assert!(outcome[0].realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.available(), ps("10"));
assert_eq!(aapl.avg_entry_price(), Some(px("150")));
}
#[test]
fn balance_adjustment_without_avg_leaves_prior_average() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("150"));
let adjustment = adj(asset("AAPL"), Some(AdjustmentAmount::Delta(ps("5"))));
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(outcome[0].average_entry_price, Some(px("150")));
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("150")));
}
#[test]
fn balance_adjustment_to_flat_clears_average_and_prunes_zero_slot() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("150"));
let adjustment = adj(asset("AAPL"), Some(AdjustmentAmount::Absolute(ps("0"))));
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(outcome.len(), 1);
assert!(outcome[0].average_entry_price.is_none());
assert!(
holdings_of(&policy, acc, &asset("AAPL")).is_none(),
"flat zero-PnL slot must not survive only because of a stale average",
);
}
#[test]
fn held_adjustment_to_net_flat_clears_average() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("150"));
let adjustment = held_adj(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("-10"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(outcome.len(), 1);
assert!(outcome[0].average_entry_price.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("slot must remain");
assert_eq!(aapl.available(), ps("10"));
assert_eq!(aapl.held(), ps("-10"));
assert!(aapl.avg_entry_price().is_none());
}
#[test]
fn held_only_adjustment_emits_no_average() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("150"));
let adjustment = held_adj(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("2"))),
None,
None,
);
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert!(outcome[0].average_entry_price.is_none());
assert!(outcome[0].realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("150")));
}
#[test]
fn buy_fill_emits_average_entry_price_and_no_pnl() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let result = run_report(&policy, &fill);
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert_eq!(aapl_entry.entry.average_entry_price, Some(px("200")));
assert!(aapl_entry.entry.realized_pnl.is_none());
let usd_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("USD"))
.expect("USD entry must exist");
assert!(usd_entry.entry.average_entry_price.is_none());
assert!(usd_entry.entry.realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("200")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn fill_without_account_currency_does_not_track_pnl_or_average() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("2")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("100"),
quantity: qty("2"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let result = run_report_without_account_currency(&policy, &fill);
assert!(result.account_blocks.is_empty());
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert!(aapl_entry.entry.average_entry_price.is_none());
assert!(aapl_entry.entry.realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.available(), ps("2"));
assert!(aapl.avg_entry_price().is_none());
assert!(aapl.realized_pnl().is_none());
}
#[test]
fn non_position_touching_fill_without_account_currency_preserves_tracking() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
let seed = TestAdjustment {
asset: asset("AAPL"),
balance: Some(AdjustmentAmount::Absolute(ps("10"))),
balance_average_entry_price: Some(px("100")),
balance_realized_pnl: Some(pnl_value("7")),
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed, &mut mutations);
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("100"),
quantity: qty("0"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let result = <TestPolicy as PreTradePolicy<
TestOrder,
TestReport,
TestAdjustment,
crate::core::FullSync,
>>::apply_execution_report(
&policy, &crate::pretrade::PostTradeContext::new(), &fill
);
assert!(
result.is_none(),
"zero-quantity fill must not emit outcomes"
);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.available(), ps("10"));
assert_eq!(aapl.avg_entry_price(), Some(px("100")));
assert_eq!(aapl.realized_pnl(), Some(pnl_value("7")));
}
#[test]
fn quote_equals_account_currency_tracks_without_market_data() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("123")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("123"),
quantity: qty("1"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("123"),
)])),
);
let result = run_report_with_currency(&policy, &fill, asset("USD"));
assert!(result.account_blocks.is_empty());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("123")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn fresh_fx_tracks_average_and_realized_pnl_in_account_currency() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let usd_eur = instr("USD", "EUR");
let b = engine_builder();
let svc = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let fx_id = svc.register(usd_eur).expect("register must succeed");
svc.push(fx_id, Quote::new().with_mark(px("0.9")))
.expect("push must succeed");
let bundle = SpotFundsMarketData::new(Arc::clone(&svc));
let policy = SpotFundsPolicy::new(settings(0), Some(bundle), b.storage_builder());
seed(&policy, acc, asset("USD"), "10000");
let buy = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &buy, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let buy_fill = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("100"),
quantity: qty("10"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let buy_result = run_report_with_currency(&policy, &buy_fill, asset("EUR"));
assert!(buy_result.account_blocks.is_empty());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("90")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
let sell = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("4")),
Some(px("120")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &sell, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let sell_fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("120"),
quantity: qty("4"),
}),
qty("0"),
true,
None,
);
let sell_result = run_report_with_currency(&policy, &sell_fill, asset("EUR"));
assert!(sell_result.account_blocks.is_empty());
let pnl = sell_result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.and_then(|o| o.entry.realized_pnl.as_ref())
.expect("realized pnl must be tracked");
assert_eq!(pnl.delta, pnl_value("72"));
assert_eq!(pnl.absolute, pnl_value("72"));
}
#[test]
fn stale_fx_quote_is_used_for_accounting() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let usd_eur = instr("USD", "EUR");
let b = engine_builder();
let svc =
MarketDataBuilder::<FullSync>::new(QuoteTtl::Within(std::time::Duration::from_millis(1)))
.build();
let fx_id = svc.register(usd_eur).expect("register must succeed");
svc.push(fx_id, Quote::new().with_mark(px("0.8")))
.expect("push must succeed");
std::thread::sleep(std::time::Duration::from_millis(5));
let bundle = SpotFundsMarketData::new(Arc::clone(&svc));
let policy = SpotFundsPolicy::new(settings(0), Some(bundle), b.storage_builder());
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("100"),
quantity: qty("1"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let result = run_report_with_currency(&policy, &fill, asset("EUR"));
assert!(result.account_blocks.is_empty());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("80")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn missing_fx_resets_tracking_without_block_or_reject() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("2")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("100"),
quantity: qty("2"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let result = run_report_with_currency(&policy, &fill, asset("EUR"));
assert!(result.account_blocks.is_empty());
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert!(aapl_entry.entry.average_entry_price.is_none());
assert!(aapl_entry.entry.realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.available(), ps("2"));
assert!(aapl.avg_entry_price().is_none());
assert!(aapl.realized_pnl().is_none());
}
#[test]
fn force_set_revives_reset_slot_then_missing_fx_resets_again() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("2")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("100"),
quantity: qty("2"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
let _ = run_report_with_currency(&policy, &fill, asset("EUR"));
let reset = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert!(reset.avg_entry_price().is_none());
assert!(reset.realized_pnl().is_none());
let force = TestAdjustment {
asset: asset("AAPL"),
balance: None,
balance_average_entry_price: Some(px("100")),
balance_realized_pnl: Some(Pnl::ZERO),
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &force, &mut mutations);
mutations.commit_all();
let revived = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(revived.avg_entry_price(), Some(px("100")));
assert_eq!(revived.realized_pnl(), Some(Pnl::ZERO));
let sell = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("1")),
Some(px("120")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &sell, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let sell_fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("120"),
quantity: qty("1"),
}),
qty("0"),
true,
None,
);
let result = run_report_with_currency(&policy, &sell_fill, asset("EUR"));
assert!(result.account_blocks.is_empty());
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert!(aapl_entry.entry.average_entry_price.is_none());
assert!(aapl_entry.entry.realized_pnl.is_none());
let reset_again = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(reset_again.available(), ps("1"));
assert!(reset_again.avg_entry_price().is_none());
assert!(reset_again.realized_pnl().is_none());
}
#[test]
fn sell_fill_against_seeded_long_realizes_pnl() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("100"));
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
None,
);
let result = run_report(&policy, &fill);
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
let pnl = aapl_entry
.entry
.realized_pnl
.as_ref()
.expect("AAPL pnl must be present");
assert_eq!(pnl.delta, pnl_value("400"));
assert_eq!(pnl.absolute, pnl_value("400"));
assert_eq!(aapl_entry.entry.average_entry_price, Some(px("100")));
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.realized_pnl(), Some(pnl_value("400")));
assert_eq!(aapl.avg_entry_price(), Some(px("100")));
}
#[test]
fn second_fill_with_same_settlement_asset_accumulates_position_accounting() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed(&policy, acc, asset("USD"), "10000");
let first_order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &first_order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let first_fill = make_report(
acc,
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("100"),
quantity: qty("1"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("100"),
)])),
);
assert!(run_report(&policy, &first_fill).account_blocks.is_empty());
let second_order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("1")),
Some(px("120")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &second_order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let second_fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("120"),
quantity: qty("1"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("120"),
)])),
);
let result = run_report(&policy, &second_fill);
assert!(
result.account_blocks.is_empty(),
"second fill in account currency must not block",
);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.available(), ps("2"));
assert_eq!(aapl.avg_entry_price(), Some(px("110")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn short_open_then_buy_to_close_realizes_pnl() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "-10", px("100"));
seed(&policy, acc, asset("USD"), "100000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("4")),
Some(px("70")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("70"),
quantity: qty("4"),
}),
qty("0"),
false,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("70"),
)])),
);
let result = run_report(&policy, &fill);
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
let pnl = aapl_entry
.entry
.realized_pnl
.as_ref()
.expect("AAPL pnl must be present");
assert_eq!(pnl.delta, pnl_value("120"));
assert_eq!(pnl.absolute, pnl_value("120"));
assert_eq!(aapl_entry.entry.average_entry_price, Some(px("100")));
}
#[test]
fn exact_close_fill_resets_average_to_none_and_keeps_realized_pnl() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("100"));
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("130")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("130"),
quantity: qty("10"),
}),
qty("0"),
true,
None,
);
let result = run_report(&policy, &fill);
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
let pnl = aapl_entry
.entry
.realized_pnl
.as_ref()
.expect("AAPL pnl must be present");
assert_eq!(pnl.delta, pnl_value("300"));
assert_eq!(pnl.absolute, pnl_value("300"));
assert!(aapl_entry.entry.average_entry_price.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("slot must survive");
assert_eq!(aapl.realized_pnl(), Some(pnl_value("300")));
assert_eq!(aapl.avg_entry_price(), None);
assert!(!aapl.is_zero());
}
#[test]
fn reservation_then_cancel_leaves_average_and_pnl_untouched() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("100"));
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("130")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let cancel = make_report(acc, aapl_usd, Side::Sell, None, qty("10"), true, None);
let _ = run_report(&policy, &cancel);
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.available(), ps("10"));
assert_eq!(aapl.held(), PositionSize::ZERO);
assert_eq!(aapl.avg_entry_price(), Some(px("100")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn adjustment_rollback_restores_prior_average() {
let acc = account(99224416);
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("100"));
let adjustment = adj_with_avg(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("5"))),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &adjustment, &mut mutations);
let after_forward = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after_forward.avg_entry_price(), Some(px("200")));
assert_eq!(after_forward.available(), ps("15"));
mutations.rollback_all();
let after_rollback = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after_rollback.avg_entry_price(), Some(px("100")));
assert_eq!(after_rollback.available(), ps("10"));
assert_eq!(after_rollback.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn balance_adjustment_force_sets_realized_pnl_and_emits_delta_and_absolute() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed_pnl = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(pnl_value("30")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed_pnl, &mut mutations);
mutations.commit_all();
let adjustment = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("0"))),
Some(pnl_value("50")),
);
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(outcome.len(), 1);
let pnl = outcome[0]
.realized_pnl
.as_ref()
.expect("realized PnL outcome must be emitted on a force-set");
assert_eq!(pnl.delta, pnl_value("20"));
assert_eq!(pnl.absolute, pnl_value("50"));
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.realized_pnl(), Some(pnl_value("50")));
}
#[test]
fn metadata_only_realized_pnl_adjustment_sets_and_emits_delta() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed_pnl = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(pnl_value("30")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed_pnl, &mut mutations);
mutations.commit_all();
let adjustment = adj_with_realized_pnl(asset("AAPL"), None, Some(pnl_value("50")));
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert_eq!(outcome.len(), 1);
assert!(outcome[0].balance.is_none());
let pnl = outcome[0]
.realized_pnl
.as_ref()
.expect("realized PnL outcome must be emitted on a force-set");
assert_eq!(pnl.delta, pnl_value("20"));
assert_eq!(pnl.absolute, pnl_value("50"));
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.realized_pnl(), Some(pnl_value("50")));
}
#[test]
fn balance_adjustment_without_realized_pnl_emits_no_pnl_and_leaves_it() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed_pnl = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(pnl_value("30")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed_pnl, &mut mutations);
mutations.commit_all();
let adjustment = adj(asset("AAPL"), Some(AdjustmentAmount::Delta(ps("5"))));
let mut mutations = Mutations::with_capacity(1);
let outcome = run_adjustment(&policy, acc, &adjustment, &mut mutations);
mutations.commit_all();
assert!(outcome[0].realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.realized_pnl(), Some(pnl_value("30")));
}
#[test]
fn adjustment_rollback_restores_realized_pnl_to_prior() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed_pnl = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(pnl_value("30")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed_pnl, &mut mutations);
mutations.commit_all();
let adjustment = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("0"))),
Some(pnl_value("50")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &adjustment, &mut mutations);
let after_forward = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after_forward.realized_pnl(), Some(pnl_value("50")));
mutations.rollback_all();
let after_rollback = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after_rollback.realized_pnl(), Some(pnl_value("30")));
}
#[test]
fn adjustment_rollback_restores_untracked_realized_pnl_to_none() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed = adj(asset("AAPL"), Some(AdjustmentAmount::Absolute(ps("10"))));
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed, &mut mutations);
mutations.commit_all();
assert_eq!(
holdings_of(&policy, acc, &asset("AAPL"))
.expect("must exist")
.realized_pnl(),
None,
);
let force = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("0"))),
Some(pnl_value("25")),
);
let mut adj_mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &force, &mut adj_mutations);
assert_eq!(
holdings_of(&policy, acc, &asset("AAPL"))
.expect("must exist")
.realized_pnl(),
Some(pnl_value("25")),
);
adj_mutations.rollback_all();
let after = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after.realized_pnl(), None);
assert_eq!(after.available(), ps("10"));
}
#[test]
fn realized_pnl_stays_untracked_after_rollback_to_none() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed = adj_with_avg(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(px("100")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed, &mut mutations);
mutations.commit_all();
assert_eq!(
holdings_of(&policy, acc, &asset("AAPL"))
.expect("must exist")
.realized_pnl(),
None,
);
let force = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("0"))),
Some(pnl_value("25")),
);
let mut adj_mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &force, &mut adj_mutations);
adj_mutations.rollback_all();
assert_eq!(
holdings_of(&policy, acc, &asset("AAPL"))
.expect("must exist")
.realized_pnl(),
None,
);
let aapl_usd = instr("AAPL", "USD");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("4")),
Some(px("130")),
);
let mut pt_mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut pt_mutations).expect("pretrade must succeed");
pt_mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("130"),
quantity: qty("4"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("130"),
)])),
);
let result = run_report(&policy, &fill);
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert!(aapl_entry.entry.realized_pnl.is_none());
let after = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after.realized_pnl(), None);
}
#[test]
fn metadata_only_average_and_pnl_roll_back_to_prior_values() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed = TestAdjustment {
asset: asset("AAPL"),
balance: Some(AdjustmentAmount::Absolute(ps("10"))),
balance_average_entry_price: Some(px("100")),
balance_realized_pnl: Some(pnl_value("30")),
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed, &mut mutations);
mutations.commit_all();
let adjustment = TestAdjustment {
asset: asset("AAPL"),
balance: None,
balance_average_entry_price: Some(px("150")),
balance_realized_pnl: Some(pnl_value("50")),
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &adjustment, &mut mutations);
let after_forward = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after_forward.avg_entry_price(), Some(px("150")));
assert_eq!(after_forward.realized_pnl(), Some(pnl_value("50")));
mutations.rollback_all();
let after_rollback = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after_rollback.avg_entry_price(), Some(px("100")));
assert_eq!(after_rollback.realized_pnl(), Some(pnl_value("30")));
}
#[test]
fn adjustment_rollback_restores_realized_pnl_snapshot_last_writer_wins() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed = TestAdjustment {
asset: asset("AAPL"),
balance: Some(AdjustmentAmount::Absolute(ps("10"))),
balance_average_entry_price: Some(px("100")),
balance_realized_pnl: Some(pnl_value("30")),
balance_lower: None,
balance_upper: None,
held: None,
held_lower: None,
held_upper: None,
incoming: None,
incoming_lower: None,
incoming_upper: None,
};
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed, &mut mutations);
mutations.commit_all();
let adjustment = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("0"))),
Some(pnl_value("50")),
);
let mut adj_mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &adjustment, &mut adj_mutations);
assert_eq!(
holdings_of(&policy, acc, &asset("AAPL"))
.expect("must exist")
.realized_pnl(),
Some(pnl_value("50")),
);
let aapl_usd = instr("AAPL", "USD");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("4")),
Some(px("130")),
);
let mut pt_mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut pt_mutations).expect("pretrade must succeed");
pt_mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("130"),
quantity: qty("4"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("130"),
)])),
);
let _ = run_report(&policy, &fill);
assert_eq!(
holdings_of(&policy, acc, &asset("AAPL"))
.expect("must exist")
.realized_pnl(),
Some(pnl_value("170")),
);
adj_mutations.rollback_all();
let after = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(after.realized_pnl(), Some(pnl_value("30")));
assert_eq!(after.available(), ps("6"));
}
#[test]
fn buy_fill_adding_to_long_recomputes_weighted_average() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("100"));
seed(&policy, acc, asset("USD"), "100000");
let order = make_order(
acc,
aapl_usd.clone(),
Side::Buy,
TradeAmount::Quantity(qty("10")),
Some(px("200")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("10"),
}),
qty("0"),
true,
Some(PreTradeLock::from_entries([(
DEFAULT_POLICY_GROUP_ID,
px("200"),
)])),
);
let result = run_report(&policy, &fill);
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
assert_eq!(aapl_entry.entry.average_entry_price, Some(px("150")));
assert!(aapl_entry.entry.realized_pnl.is_none());
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("150")));
assert_eq!(aapl.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn sell_fill_flipping_long_to_short_realizes_and_reopens_at_price() {
let acc = account(99224416);
let aapl_usd = instr("AAPL", "USD");
let policy = build_policy(None, None);
seed_with_avg(&policy, acc, asset("AAPL"), "10", px("100"));
let order = make_order(
acc,
aapl_usd.clone(),
Side::Sell,
TradeAmount::Quantity(qty("10")),
Some(px("130")),
);
let mut mutations = Mutations::with_capacity(1);
pre_trade_check(&policy, &order, &mut mutations).expect("pretrade must succeed");
mutations.commit_all();
let fill = make_report(
acc,
aapl_usd,
Side::Sell,
Some(Trade {
price: px("130"),
quantity: qty("15"),
}),
qty("0"),
true,
None,
);
let result = run_report(&policy, &fill);
let aapl_entry = result
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL entry must exist");
let pnl = aapl_entry
.entry
.realized_pnl
.as_ref()
.expect("AAPL pnl must be present");
assert_eq!(pnl.delta, pnl_value("300"));
assert_eq!(pnl.absolute, pnl_value("300"));
assert_eq!(aapl_entry.entry.average_entry_price, Some(px("130")));
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.avg_entry_price(), Some(px("130")));
assert_eq!(aapl.realized_pnl(), Some(pnl_value("300")));
}
#[test]
fn batch_force_setting_realized_pnl_then_rejected_rolls_back_to_prior() {
let acc = account(99224416);
let policy = build_policy(None, None);
let seed_pnl = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(pnl_value("30")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &seed_pnl, &mut mutations);
mutations.commit_all();
let force = adj_with_realized_pnl(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("0"))),
Some(pnl_value("50")),
);
let mut mutations = Mutations::with_capacity(1);
let _ = run_adjustment(&policy, acc, &force, &mut mutations);
let rejecting = bounded_adj(
asset("AAPL"),
Some(AdjustmentAmount::Delta(ps("1"))),
None,
Some(ps("0")),
);
let mut reject_mutations = Mutations::with_capacity(1);
let rejected = run_adjustment_result(&policy, acc, &rejecting, &mut reject_mutations);
assert!(rejected.is_err());
mutations.rollback_all();
let aapl = holdings_of(&policy, acc, &asset("AAPL")).expect("must exist");
assert_eq!(aapl.realized_pnl(), Some(pnl_value("30")));
}