use rust_decimal::Decimal;
use crate::param::{AdjustmentAmount, Pnl, PositionSize, Price};
use super::error::{AdjustmentOverflowError, HoldError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holdings {
available: PositionSize,
held: PositionSize,
incoming: PositionSize,
avg_entry_price: Option<Price>,
realized_pnl: Option<Pnl>,
}
impl Default for Holdings {
fn default() -> Self {
Self::zero()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AdjustmentTarget {
Available,
Held,
Incoming,
}
impl Holdings {
pub fn zero() -> Self {
Self {
avg_entry_price: None,
available: PositionSize::ZERO,
held: PositionSize::ZERO,
incoming: PositionSize::ZERO,
realized_pnl: None,
}
}
pub fn new(available: PositionSize, held: PositionSize) -> Self {
Self {
avg_entry_price: None,
available,
held,
incoming: PositionSize::ZERO,
realized_pnl: None,
}
}
pub fn available(&self) -> PositionSize {
self.available
}
pub fn held(&self) -> PositionSize {
self.held
}
pub fn incoming(&self) -> PositionSize {
self.incoming
}
pub fn avg_entry_price(&self) -> Option<Price> {
self.avg_entry_price
}
pub fn realized_pnl(&self) -> Option<Pnl> {
self.realized_pnl
}
pub fn with_realized_pnl(&self, realized_pnl: Pnl) -> Self {
Self {
realized_pnl: Some(realized_pnl),
..*self
}
}
pub fn with_realized_pnl_opt(&self, realized_pnl: Option<Pnl>) -> Self {
Self {
realized_pnl,
..*self
}
}
pub fn without_position_tracking(&self) -> Self {
Self {
avg_entry_price: None,
realized_pnl: None,
..*self
}
}
pub fn try_hold(&self, amount: PositionSize) -> Result<Self, HoldError> {
let spendable = if self.held < PositionSize::ZERO {
self.available
.checked_add(self.held)
.map_err(|_| HoldError::ArithmeticOverflow)?
} else {
self.available
};
if amount > spendable {
return Err(HoldError::InsufficientAvailable {
available: spendable,
requested: amount,
});
}
let available = self
.available
.checked_sub(amount)
.map_err(|_| HoldError::ArithmeticOverflow)?;
let held = self
.held
.checked_add(amount)
.map_err(|_| HoldError::ArithmeticOverflow)?;
Ok(Self {
avg_entry_price: self.avg_entry_price,
available,
held,
incoming: self.incoming,
realized_pnl: self.realized_pnl,
})
}
pub fn release(&self, amount: PositionSize) -> Result<Self, AdjustmentOverflowError> {
let available = self
.available
.checked_add(amount)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
let held = self
.held
.checked_sub(amount)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
Ok(Self {
avg_entry_price: self.avg_entry_price,
available,
held,
incoming: self.incoming,
realized_pnl: self.realized_pnl,
})
}
pub fn apply_fill_outflow(
&self,
amount: PositionSize,
) -> Result<Self, AdjustmentOverflowError> {
let held = self
.held
.checked_sub(amount)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
Ok(Self {
avg_entry_price: self.avg_entry_price,
available: self.available,
held,
incoming: self.incoming,
realized_pnl: self.realized_pnl,
})
}
pub fn apply_fill_inflow(&self, amount: PositionSize) -> Result<Self, AdjustmentOverflowError> {
let available = self
.available
.checked_add(amount)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
Ok(Self {
avg_entry_price: self.avg_entry_price,
available,
held: self.held,
incoming: self.incoming,
realized_pnl: self.realized_pnl,
})
}
pub fn realize_position_fill(
&self,
signed_qty: PositionSize,
price: Price,
) -> Result<(Self, Option<Pnl>), AdjustmentOverflowError> {
let owned = self
.available
.checked_add(self.held)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
let new_owned = owned
.checked_add(signed_qty)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
if !owned.is_zero() && self.realized_pnl.is_none() {
return Ok((self.without_position_tracking(), None));
}
let owned_dec = owned.to_decimal();
let delta_dec = signed_qty.to_decimal();
let price_dec = price.to_decimal();
let zero = Decimal::ZERO;
let (new_avg, realized_dec) = if owned_dec == zero {
let avg = if delta_dec == zero { None } else { Some(price) };
(avg, zero)
} else if (owned_dec > zero) == (delta_dec > zero) {
match self.avg_entry_price {
Some(avg) => {
let weighted_existing = owned_dec
.checked_mul(avg.to_decimal())
.ok_or(AdjustmentOverflowError::ArithmeticOverflow)?;
let weighted_fill = delta_dec
.checked_mul(price_dec)
.ok_or(AdjustmentOverflowError::ArithmeticOverflow)?;
let numerator = weighted_existing
.checked_add(weighted_fill)
.ok_or(AdjustmentOverflowError::ArithmeticOverflow)?;
let new_avg_dec = numerator
.checked_div(new_owned.to_decimal())
.ok_or(AdjustmentOverflowError::ArithmeticOverflow)?;
(Some(Price::new(new_avg_dec)), zero)
}
None => (None, zero),
}
} else {
match self.avg_entry_price {
Some(avg) => {
let price_minus_avg = price_dec
.checked_sub(avg.to_decimal())
.ok_or(AdjustmentOverflowError::ArithmeticOverflow)?;
if delta_dec.abs() <= owned_dec.abs() {
let closed_qty = -delta_dec;
let realized = price_minus_avg
.checked_mul(closed_qty)
.ok_or(AdjustmentOverflowError::ArithmeticOverflow)?;
let avg = if new_owned.is_zero() { None } else { Some(avg) };
(avg, realized)
} else {
let realized = price_minus_avg
.checked_mul(owned_dec)
.ok_or(AdjustmentOverflowError::ArithmeticOverflow)?;
(Some(price), realized)
}
}
None => {
let new_avg = if delta_dec.abs() <= owned_dec.abs() {
None
} else {
Some(price)
};
(new_avg, zero)
}
}
};
let realized_delta = Pnl::new(realized_dec);
let realized_pnl = match self.realized_pnl {
Some(realized_pnl) => Some(
realized_pnl
.checked_add(realized_delta)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?,
),
None => Some(realized_delta),
};
Ok((
Self {
avg_entry_price: new_avg,
available: self.available,
held: self.held,
incoming: self.incoming,
realized_pnl,
},
Some(realized_delta),
))
}
pub fn apply_delta_rollback(
&self,
available_delta: PositionSize,
held_delta: PositionSize,
incoming_delta: PositionSize,
) -> Result<Self, AdjustmentOverflowError> {
let available = self
.available
.checked_sub(available_delta)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
let held = self
.held
.checked_sub(held_delta)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
let incoming = self
.incoming
.checked_sub(incoming_delta)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?;
Ok(Self {
avg_entry_price: self.avg_entry_price,
available,
held,
incoming,
realized_pnl: self.realized_pnl,
})
}
pub fn apply_adjustment(
&self,
target: AdjustmentTarget,
amount: AdjustmentAmount,
) -> Result<Self, AdjustmentOverflowError> {
let mut new = *self;
let field = match target {
AdjustmentTarget::Available => &mut new.available,
AdjustmentTarget::Held => &mut new.held,
AdjustmentTarget::Incoming => &mut new.incoming,
};
*field = match amount {
AdjustmentAmount::Absolute(v) => v,
AdjustmentAmount::Delta(d) => field
.checked_add(d)
.map_err(|_| AdjustmentOverflowError::ArithmeticOverflow)?,
};
Ok(new)
}
pub fn with_avg_entry_price(&self, avg_entry_price: Option<Price>) -> Self {
Self {
avg_entry_price,
..*self
}
}
pub fn is_zero(&self) -> bool {
self.available.is_zero()
&& self.held.is_zero()
&& self.incoming.is_zero()
&& self.realized_pnl.map_or(true, |pnl| pnl.is_zero())
&& self.avg_entry_price.is_none()
}
pub fn available_within_bounds(
&self,
lower: Option<PositionSize>,
upper: Option<PositionSize>,
) -> bool {
!lower.is_some_and(|b| self.available < b) && !upper.is_some_and(|b| self.available > b)
}
pub fn held_within_bounds(
&self,
lower: Option<PositionSize>,
upper: Option<PositionSize>,
) -> bool {
!lower.is_some_and(|b| self.held < b) && !upper.is_some_and(|b| self.held > b)
}
pub fn incoming_within_bounds(
&self,
lower: Option<PositionSize>,
upper: Option<PositionSize>,
) -> bool {
!lower.is_some_and(|b| self.incoming < b) && !upper.is_some_and(|b| self.incoming > b)
}
}
#[cfg(test)]
mod tests {
use rust_decimal::Decimal;
use crate::param::{AdjustmentAmount, Pnl, PositionSize, Price};
use super::super::error::{AdjustmentOverflowError, HoldError};
use super::{AdjustmentTarget, Holdings};
fn ps(value: &str) -> PositionSize {
PositionSize::from_str(value).expect("position size literal must be valid")
}
fn pnl(value: &str) -> Pnl {
Pnl::from_str(value).expect("pnl literal must be valid")
}
fn px(value: &str) -> Price {
Price::from_str(value).expect("price literal must be valid")
}
fn holdings(available: &str, held: &str) -> Holdings {
Holdings::new(ps(available), ps(held))
}
fn max_ps() -> PositionSize {
PositionSize::new(Decimal::MAX)
}
fn min_ps() -> PositionSize {
PositionSize::new(Decimal::MIN)
}
#[test]
fn zero_returns_empty_components() {
let value = Holdings::zero();
assert_eq!(value.available(), PositionSize::ZERO);
assert_eq!(value.held(), PositionSize::ZERO);
assert_eq!(value.incoming(), PositionSize::ZERO);
}
#[test]
fn new_stores_explicit_components() {
let value = Holdings::new(ps("5"), ps("3"));
assert_eq!(value.available(), ps("5"));
assert_eq!(value.held(), ps("3"));
assert_eq!(value.incoming(), PositionSize::ZERO);
assert_eq!(
Holdings::new(PositionSize::ZERO, PositionSize::ZERO),
Holdings::zero(),
);
}
#[test]
fn new_accepts_negative_components() {
let value = Holdings::new(ps("-1"), ps("-2"));
assert_eq!(value.available(), ps("-1"));
assert_eq!(value.held(), ps("-2"));
assert_eq!(value.incoming(), PositionSize::ZERO);
}
#[test]
fn accessors_return_constructor_values() {
let value = holdings("7", "4");
assert_eq!(value.available(), ps("7"));
assert_eq!(value.held(), ps("4"));
assert_eq!(value.incoming(), PositionSize::ZERO);
}
#[test]
fn try_hold_moves_available_to_held() {
let value = holdings("10", "0");
let updated = value.try_hold(ps("5")).expect("must hold");
assert_eq!(updated.available(), ps("5"));
assert_eq!(updated.held(), ps("5"));
}
#[test]
fn try_hold_all_available() {
let value = holdings("10", "0");
let updated = value.try_hold(ps("10")).expect("must hold");
assert_eq!(updated.available(), PositionSize::ZERO);
assert_eq!(updated.held(), ps("10"));
}
#[test]
fn try_hold_rejects_insufficient_available_without_changing_original() {
let value = holdings("10", "0");
let err = value.try_hold(ps("15")).expect_err("must fail");
assert_eq!(
err,
HoldError::InsufficientAvailable {
available: ps("10"),
requested: ps("15"),
}
);
assert_eq!(value, holdings("10", "0"));
}
#[test]
fn try_hold_negative_amount_inverts_as_arithmetic() {
let value = holdings("10", "5");
let updated = value.try_hold(ps("-3")).expect("must succeed");
assert_eq!(updated.available(), ps("13"));
assert_eq!(updated.held(), ps("2"));
}
#[test]
fn try_hold_reports_arithmetic_overflow_when_held_would_overflow() {
let value = Holdings::new(max_ps(), max_ps());
let err = value.try_hold(max_ps()).expect_err("must fail");
assert_eq!(err, HoldError::ArithmeticOverflow);
}
#[test]
fn try_hold_respects_negative_held() {
let value = Holdings::new(ps("2000"), ps("-2000"));
let err = value.try_hold(ps("1")).expect_err("must reject");
assert_eq!(
err,
HoldError::InsufficientAvailable {
available: PositionSize::ZERO,
requested: ps("1"),
}
);
}
#[test]
fn try_hold_succeeds_when_negative_held_covered_by_available() {
let value = Holdings::new(ps("5000"), ps("-2000"));
value
.try_hold(ps("3000"))
.expect("must succeed within spendable");
let err = value
.try_hold(ps("3001"))
.expect_err("must reject one over");
assert_eq!(
err,
HoldError::InsufficientAvailable {
available: ps("3000"),
requested: ps("3001"),
}
);
}
#[test]
fn try_hold_positive_held_does_not_change_spendable() {
let value = holdings("10", "5");
value
.try_hold(ps("10"))
.expect("must succeed - held is positive, spendable = available");
}
#[test]
fn release_moves_held_to_available() {
let value = holdings("2", "10");
let updated = value.release(ps("4")).expect("must release");
assert_eq!(updated.available(), ps("6"));
assert_eq!(updated.held(), ps("6"));
}
#[test]
fn release_all_held() {
let value = holdings("2", "10");
let updated = value.release(ps("10")).expect("must release");
assert_eq!(updated.available(), ps("12"));
assert_eq!(updated.held(), PositionSize::ZERO);
}
#[test]
fn release_amount_exceeding_held_drives_held_negative() {
let value = holdings("2", "10");
let updated = value.release(ps("15")).expect("must succeed");
assert_eq!(updated.available(), ps("17"));
assert_eq!(updated.held(), ps("-5"));
}
#[test]
fn release_negative_amount_inverts_as_arithmetic() {
let value = holdings("10", "5");
let updated = value.release(ps("-3")).expect("must succeed");
assert_eq!(updated.available(), ps("7"));
assert_eq!(updated.held(), ps("8"));
}
#[test]
fn release_reports_arithmetic_overflow_when_available_would_overflow() {
let value = Holdings::new(max_ps(), max_ps());
let err = value.release(max_ps()).expect_err("must fail");
assert_eq!(err, AdjustmentOverflowError::ArithmeticOverflow);
}
#[test]
fn apply_fill_outflow_subtracts_held_only() {
let value = holdings("10", "5");
let updated = value.apply_fill_outflow(ps("3")).expect("must subtract");
assert_eq!(updated.available(), ps("10"));
assert_eq!(updated.held(), ps("2"));
}
#[test]
fn apply_fill_outflow_drives_held_negative_when_amount_exceeds_held() {
let value = holdings("10", "5");
let updated = value.apply_fill_outflow(ps("8")).expect("must subtract");
assert_eq!(updated.available(), ps("10"));
assert_eq!(updated.held(), ps("-3"));
}
#[test]
fn apply_fill_outflow_negative_amount_adds_to_held() {
let value = holdings("10", "5");
let updated = value.apply_fill_outflow(ps("-3")).expect("must succeed");
assert_eq!(updated.available(), ps("10"));
assert_eq!(updated.held(), ps("8"));
}
#[test]
fn apply_fill_outflow_reports_arithmetic_overflow() {
let value = Holdings::new(PositionSize::ZERO, max_ps());
let err = value
.apply_fill_outflow(min_ps())
.expect_err("must overflow");
assert_eq!(err, AdjustmentOverflowError::ArithmeticOverflow);
}
#[test]
fn apply_fill_inflow_zero_amount_is_no_change() {
let value = holdings("10", "2");
let updated = value
.apply_fill_inflow(PositionSize::ZERO)
.expect("must succeed");
assert_eq!(updated, value);
}
#[test]
fn apply_fill_inflow_adds_to_available_only() {
let value = holdings("10", "5");
let updated = value.apply_fill_inflow(ps("3")).expect("must add");
assert_eq!(updated.available(), ps("13"));
assert_eq!(updated.held(), ps("5"));
}
#[test]
fn apply_fill_inflow_accepts_negative_amount_driving_available_negative() {
let value = holdings("3", "5");
let updated = value.apply_fill_inflow(ps("-7")).expect("must add");
assert_eq!(updated.available(), ps("-4"));
assert_eq!(updated.held(), ps("5"));
}
#[test]
fn apply_fill_inflow_reports_arithmetic_overflow() {
let value = Holdings::new(max_ps(), PositionSize::ZERO);
let err = value
.apply_fill_inflow(max_ps())
.expect_err("must overflow");
assert_eq!(err, AdjustmentOverflowError::ArithmeticOverflow);
}
#[test]
fn apply_adjustment_sets_available_absolute_values() {
let value = holdings("5", "11");
assert_eq!(
value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Absolute(ps("7"))
)
.expect("absolute must succeed")
.available(),
ps("7")
);
assert_eq!(
value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Absolute(ps("0"))
)
.expect("absolute must succeed")
.available(),
PositionSize::ZERO
);
assert_eq!(
value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Absolute(ps("7"))
)
.expect("absolute must succeed")
.held(),
ps("11")
);
let neg = value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Absolute(ps("-1")),
)
.expect("absolute must succeed");
assert_eq!(neg.available(), ps("-1"));
assert_eq!(neg.held(), ps("11"));
}
#[test]
fn apply_adjustment_sets_held_absolute_values() {
let value = holdings("11", "5");
assert_eq!(
value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Absolute(ps("7")))
.expect("absolute must succeed")
.held(),
ps("7")
);
assert_eq!(
value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Absolute(ps("0")))
.expect("absolute must succeed")
.held(),
PositionSize::ZERO
);
assert_eq!(
value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Absolute(ps("7")))
.expect("absolute must succeed")
.available(),
ps("11")
);
let neg = value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Absolute(ps("-1")))
.expect("absolute must succeed");
assert_eq!(neg.held(), ps("-1"));
assert_eq!(neg.available(), ps("11"));
}
#[test]
fn apply_adjustment_applies_available_deltas() {
let value = holdings("5", "11");
assert_eq!(
value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Delta(ps("3"))
)
.expect("delta must succeed"),
holdings("8", "11")
);
assert_eq!(
value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Delta(ps("0"))
)
.expect("delta must succeed"),
value
);
assert_eq!(
value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Delta(ps("-3"))
)
.expect("delta must succeed"),
holdings("2", "11")
);
assert_eq!(
value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Delta(ps("-5"))
)
.expect("delta must succeed"),
holdings("0", "11")
);
let neg = value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Delta(ps("-6")),
)
.expect("delta must succeed");
assert_eq!(neg.available(), ps("-1"));
assert_eq!(neg.held(), ps("11"));
}
#[test]
fn apply_adjustment_applies_held_deltas() {
let value = holdings("11", "5");
assert_eq!(
value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Delta(ps("3")))
.expect("delta must succeed"),
holdings("11", "8")
);
assert_eq!(
value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Delta(ps("0")))
.expect("delta must succeed"),
value
);
assert_eq!(
value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Delta(ps("-3")))
.expect("delta must succeed"),
holdings("11", "2")
);
assert_eq!(
value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Delta(ps("-5")))
.expect("delta must succeed"),
holdings("11", "0")
);
let neg = value
.apply_adjustment(AdjustmentTarget::Held, AdjustmentAmount::Delta(ps("-6")))
.expect("delta must succeed");
assert_eq!(neg.held(), ps("-1"));
assert_eq!(neg.available(), ps("11"));
}
#[test]
fn apply_adjustment_reports_arithmetic_overflow_for_delta() {
let value = Holdings::new(max_ps(), PositionSize::ZERO);
let err = value
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Delta(max_ps()),
)
.expect_err("must overflow");
assert_eq!(err, AdjustmentOverflowError::ArithmeticOverflow);
}
#[test]
fn apply_adjustment_sets_incoming_absolute_values() {
let value = holdings("5", "11");
let set = value
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Absolute(ps("7")),
)
.expect("absolute must succeed");
assert_eq!(set.incoming(), ps("7"));
assert_eq!(set.available(), ps("5"));
assert_eq!(set.held(), ps("11"));
let zero = value
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Absolute(ps("0")),
)
.expect("absolute must succeed");
assert_eq!(zero.incoming(), PositionSize::ZERO);
let neg = value
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Absolute(ps("-3")),
)
.expect("absolute must succeed");
assert_eq!(neg.incoming(), ps("-3"));
assert_eq!(neg.available(), ps("5"));
assert_eq!(neg.held(), ps("11"));
}
#[test]
fn apply_adjustment_applies_incoming_deltas() {
let mut base = holdings("5", "11");
base = base
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Absolute(ps("10")),
)
.expect("seed must succeed");
assert_eq!(
base.apply_adjustment(AdjustmentTarget::Incoming, AdjustmentAmount::Delta(ps("3")))
.expect("delta must succeed")
.incoming(),
ps("13")
);
assert_eq!(
base.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Delta(ps("-4"))
)
.expect("delta must succeed")
.incoming(),
ps("6")
);
let neg = base
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Delta(ps("-15")),
)
.expect("delta must succeed");
assert_eq!(neg.incoming(), ps("-5"));
assert_eq!(neg.available(), ps("5"));
assert_eq!(neg.held(), ps("11"));
}
#[test]
fn apply_adjustment_incoming_overflow_returns_error() {
let mut value = Holdings::new(PositionSize::ZERO, PositionSize::ZERO);
value = value
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Absolute(max_ps()),
)
.expect("seed must succeed");
let err = value
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Delta(max_ps()),
)
.expect_err("must overflow");
assert_eq!(err, AdjustmentOverflowError::ArithmeticOverflow);
}
#[test]
fn trading_operations_do_not_touch_incoming() {
let mut base = holdings("10", "5");
base = base
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Absolute(ps("7")),
)
.expect("seed must succeed");
assert_eq!(
base.try_hold(ps("3")).expect("must hold").incoming(),
ps("7")
);
assert_eq!(
base.release(ps("2")).expect("must release").incoming(),
ps("7")
);
assert_eq!(
base.apply_fill_outflow(ps("2"))
.expect("must outflow")
.incoming(),
ps("7")
);
assert_eq!(
base.apply_fill_inflow(ps("2"))
.expect("must inflow")
.incoming(),
ps("7")
);
}
#[test]
fn available_within_bounds_accepts_missing_bounds() {
assert!(holdings("5", "0").available_within_bounds(None, None));
}
#[test]
fn available_within_bounds_checks_lower_inclusively() {
assert!(holdings("5", "0").available_within_bounds(Some(ps("3")), None));
assert!(!holdings("2", "0").available_within_bounds(Some(ps("3")), None));
assert!(holdings("3", "0").available_within_bounds(Some(ps("3")), None));
}
#[test]
fn available_within_bounds_checks_upper_inclusively() {
assert!(holdings("5", "0").available_within_bounds(None, Some(ps("7"))));
assert!(!holdings("8", "0").available_within_bounds(None, Some(ps("7"))));
assert!(holdings("7", "0").available_within_bounds(None, Some(ps("7"))));
}
#[test]
fn available_within_bounds_checks_both_bounds() {
assert!(holdings("5", "0").available_within_bounds(Some(ps("3")), Some(ps("7"))));
assert!(!holdings("2", "0").available_within_bounds(Some(ps("3")), Some(ps("7"))));
assert!(!holdings("8", "0").available_within_bounds(Some(ps("3")), Some(ps("7"))));
}
#[test]
fn available_within_bounds_handles_negative_bounds() {
assert!(holdings("0", "0").available_within_bounds(Some(ps("-3")), None));
assert!(!holdings("0", "0").available_within_bounds(Some(ps("1")), None));
}
#[test]
fn held_within_bounds_checks_inclusively() {
let h = holdings("0", "5");
assert!(h.held_within_bounds(None, None));
assert!(h.held_within_bounds(Some(ps("3")), None));
assert!(!h.held_within_bounds(Some(ps("6")), None));
assert!(h.held_within_bounds(Some(ps("5")), None));
assert!(h.held_within_bounds(None, Some(ps("7"))));
assert!(!h.held_within_bounds(None, Some(ps("4"))));
assert!(h.held_within_bounds(None, Some(ps("5"))));
assert!(h.held_within_bounds(Some(ps("3")), Some(ps("7"))));
assert!(!h.held_within_bounds(Some(ps("6")), Some(ps("9"))));
}
#[test]
fn incoming_within_bounds_checks_inclusively() {
let mut base = holdings("0", "0");
base = base
.apply_adjustment(
AdjustmentTarget::Incoming,
AdjustmentAmount::Absolute(ps("5")),
)
.expect("seed must succeed");
assert!(base.incoming_within_bounds(None, None));
assert!(base.incoming_within_bounds(Some(ps("3")), None));
assert!(!base.incoming_within_bounds(Some(ps("6")), None));
assert!(base.incoming_within_bounds(Some(ps("5")), None));
assert!(base.incoming_within_bounds(None, Some(ps("7"))));
assert!(!base.incoming_within_bounds(None, Some(ps("4"))));
assert!(base.incoming_within_bounds(None, Some(ps("5"))));
assert!(base.incoming_within_bounds(Some(ps("3")), Some(ps("7"))));
assert!(!base.incoming_within_bounds(Some(ps("6")), Some(ps("9"))));
}
#[test]
fn holdings_is_copy() {
let original = holdings("10", "5");
let copied = original;
assert_eq!(copied, original);
}
#[test]
fn mutating_operations_return_new_values() {
let original = holdings("10", "5");
let held = original.try_hold(ps("3")).expect("must hold");
let released = original.release(ps("2")).expect("must release");
let outflow = original.apply_fill_outflow(ps("2")).expect("must subtract");
let inflow = original.apply_fill_inflow(ps("2")).expect("must add");
assert_eq!(original, holdings("10", "5"));
assert_eq!(held, holdings("7", "8"));
assert_eq!(released, holdings("12", "3"));
assert_eq!(outflow, holdings("10", "3"));
assert_eq!(inflow, holdings("12", "5"));
}
#[test]
fn new_and_zero_have_no_avg_and_untracked_pnl() {
let zero = Holdings::zero();
assert_eq!(zero.avg_entry_price(), None);
assert_eq!(zero.realized_pnl(), None);
let made = Holdings::new(ps("5"), ps("3"));
assert_eq!(made.avg_entry_price(), None);
assert_eq!(made.realized_pnl(), None);
}
#[test]
fn realize_open_from_flat_seeds_avg_and_realizes_nothing() {
let flat = Holdings::zero();
let (updated, realized) = flat
.realize_position_fill(ps("10"), px("100"))
.expect("must realize");
assert_eq!(realized, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), Some(px("100")));
assert_eq!(updated.realized_pnl(), Some(Pnl::ZERO));
}
#[test]
fn realize_open_short_from_flat_seeds_avg() {
let flat = Holdings::zero();
let (updated, realized) = flat
.realize_position_fill(ps("-4"), px("50"))
.expect("must realize");
assert_eq!(realized, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), Some(px("50")));
}
#[test]
fn realize_zero_qty_from_flat_keeps_avg_none() {
let flat = Holdings::zero();
let (updated, realized) = flat
.realize_position_fill(PositionSize::ZERO, px("100"))
.expect("must realize");
assert_eq!(realized, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), None);
}
#[test]
fn realize_add_to_long_weights_average() {
let long = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = long
.realize_position_fill(ps("10"), px("200"))
.expect("must realize");
assert_eq!(realized, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), Some(px("150")));
}
#[test]
fn realize_add_to_short_weights_average() {
let short = Holdings::new(ps("-10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = short
.realize_position_fill(ps("-10"), px("200"))
.expect("must realize");
assert_eq!(realized, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), Some(px("150")));
}
#[test]
fn realize_partial_close_long_realizes_positive_when_price_above_avg() {
let long = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = long
.realize_position_fill(ps("-4"), px("130"))
.expect("must realize");
assert_eq!(realized, Some(pnl("120")));
assert_eq!(updated.avg_entry_price(), Some(px("100")));
assert_eq!(updated.realized_pnl(), Some(pnl("120")));
}
#[test]
fn realize_partial_close_short_realizes_positive_when_price_below_avg() {
let short = Holdings::new(ps("-10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = short
.realize_position_fill(ps("4"), px("70"))
.expect("must realize");
assert_eq!(realized, Some(pnl("120")));
assert_eq!(updated.avg_entry_price(), Some(px("100")));
}
#[test]
fn realize_exact_close_long_resets_avg_to_none_and_keeps_pnl() {
let long = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = long
.realize_position_fill(ps("-10"), px("130"))
.expect("must realize");
assert_eq!(realized, Some(pnl("300")));
assert_eq!(updated.avg_entry_price(), None);
assert_eq!(updated.realized_pnl(), Some(pnl("300")));
}
#[test]
fn realize_flip_long_to_short_closes_then_reopens_at_price() {
let long = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = long
.realize_position_fill(ps("-15"), px("130"))
.expect("must realize");
assert_eq!(realized, Some(pnl("300")));
assert_eq!(updated.avg_entry_price(), Some(px("130")));
}
#[test]
fn realize_flip_short_to_long_closes_then_reopens_at_price() {
let short = Holdings::new(ps("-10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = short
.realize_position_fill(ps("15"), px("70"))
.expect("must realize");
assert_eq!(realized, Some(pnl("300")));
assert_eq!(updated.avg_entry_price(), Some(px("70")));
}
#[test]
fn realize_partial_close_long_at_loss_is_negative() {
let long = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (_updated, realized) = long
.realize_position_fill(ps("-4"), px("80"))
.expect("must realize");
assert_eq!(realized, Some(pnl("-80")));
}
#[test]
fn realize_owned_uses_available_plus_held() {
let long = Holdings::new(ps("6"), ps("4"))
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, realized) = long
.realize_position_fill(ps("-10"), px("130"))
.expect("must realize");
assert_eq!(realized, Some(pnl("300")));
assert_eq!(updated.avg_entry_price(), None);
}
#[test]
fn realize_reduce_without_basis_realizes_nothing_and_keeps_no_average() {
let basis_less = Holdings::new(ps("10"), PositionSize::ZERO);
let (updated, realized) = basis_less
.realize_position_fill(ps("-4"), px("200"))
.expect("must realize");
assert_eq!(realized, None);
assert_eq!(updated.avg_entry_price(), None);
assert_eq!(updated.realized_pnl(), None);
}
#[test]
fn realize_exact_close_without_basis_realizes_nothing() {
let basis_less = Holdings::new(ps("-10"), PositionSize::ZERO);
let (updated, realized) = basis_less
.realize_position_fill(ps("10"), px("70"))
.expect("must realize");
assert_eq!(realized, None);
assert_eq!(updated.avg_entry_price(), None);
}
#[test]
fn realize_add_without_basis_stays_basis_less() {
let basis_less = Holdings::new(ps("10"), PositionSize::ZERO);
let (updated, realized) = basis_less
.realize_position_fill(ps("5"), px("200"))
.expect("must realize");
assert_eq!(realized, None);
assert_eq!(updated.avg_entry_price(), None);
}
#[test]
fn realize_flip_without_basis_opens_remainder_at_price() {
let basis_less = Holdings::new(ps("10"), PositionSize::ZERO);
let (updated, realized) = basis_less
.realize_position_fill(ps("-15"), px("130"))
.expect("must realize");
assert_eq!(realized, None);
assert_eq!(updated.avg_entry_price(), None);
}
#[test]
fn realize_after_rollback_to_none_stays_untracked() {
let rolled_back = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl_opt(None);
let (updated, realized) = rolled_back
.realize_position_fill(ps("-4"), px("130"))
.expect("must realize");
assert_eq!(realized, None);
assert_eq!(updated.realized_pnl(), None);
assert_eq!(updated.avg_entry_price(), None);
}
#[test]
fn realize_accumulates_realized_pnl_across_fills() {
let long = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(pnl("50"));
let (updated, realized) = long
.realize_position_fill(ps("-4"), px("130"))
.expect("must realize");
assert_eq!(realized, Some(pnl("120")));
assert_eq!(updated.realized_pnl(), Some(pnl("170")));
}
#[test]
fn realize_position_fill_leaves_quantities_untouched() {
let long = Holdings::new(ps("10"), ps("2"))
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(Pnl::ZERO);
let (updated, _realized) = long
.realize_position_fill(ps("-4"), px("130"))
.expect("must realize");
assert_eq!(updated.available(), ps("10"));
assert_eq!(updated.held(), ps("2"));
assert_eq!(updated.incoming(), PositionSize::ZERO);
}
#[test]
fn realize_position_fill_reports_overflow() {
let long = Holdings::new(max_ps(), PositionSize::ZERO)
.with_avg_entry_price(Some(px("2")))
.with_realized_pnl(Pnl::ZERO);
let err = long
.realize_position_fill(max_ps(), px("2"))
.expect_err("must overflow");
assert_eq!(err, AdjustmentOverflowError::ArithmeticOverflow);
}
#[test]
fn is_zero_requires_no_avg_and_zero_realized_pnl() {
assert!(Holdings::zero().is_zero());
let with_pnl = Holdings::zero().with_realized_pnl(pnl("5"));
assert!(!with_pnl.is_zero());
let with_zero_pnl = Holdings::zero().with_realized_pnl(Pnl::ZERO);
assert!(with_zero_pnl.is_zero());
let with_avg = Holdings::zero().with_avg_entry_price(Some(px("100")));
assert!(!with_avg.is_zero());
}
#[test]
fn reservation_and_cancel_preserve_avg_and_pnl() {
let base = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(pnl("7"));
let held = base.try_hold(ps("4")).expect("must hold");
assert_eq!(held.avg_entry_price(), Some(px("100")));
assert_eq!(held.realized_pnl(), Some(pnl("7")));
let released = held.release(ps("4")).expect("must release");
assert_eq!(released.avg_entry_price(), Some(px("100")));
assert_eq!(released.realized_pnl(), Some(pnl("7")));
}
#[test]
fn apply_delta_rollback_reverses_quantity_deltas() {
let slot = Holdings::new(ps("10"), ps("2"))
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(pnl("90"));
let rolled = slot
.apply_delta_rollback(ps("3"), ps("1"), PositionSize::ZERO)
.expect("rollback must succeed");
assert_eq!(rolled.available(), ps("7"));
assert_eq!(rolled.held(), ps("1"));
assert_eq!(rolled.incoming(), PositionSize::ZERO);
assert_eq!(rolled.avg_entry_price(), Some(px("100")));
assert_eq!(rolled.realized_pnl(), Some(pnl("90")));
}
#[test]
fn apply_delta_rollback_leaves_avg_and_pnl_untouched() {
let slot = Holdings::new(ps("5"), ps("0"))
.with_avg_entry_price(Some(px("42")))
.with_realized_pnl(pnl("42"));
let rolled = slot
.apply_delta_rollback(ps("3"), PositionSize::ZERO, PositionSize::ZERO)
.expect("rollback must succeed");
assert_eq!(rolled.available(), ps("2"));
assert_eq!(rolled.avg_entry_price(), Some(px("42")));
assert_eq!(rolled.realized_pnl(), Some(pnl("42")));
}
#[test]
fn with_realized_pnl_opt_restores_untracked_state() {
let tracked = Holdings::new(ps("10"), ps("0")).with_realized_pnl(pnl("5"));
let untracked = tracked.with_realized_pnl_opt(None);
assert_eq!(untracked.realized_pnl(), None);
assert_eq!(untracked.available(), ps("10"));
let retracked = untracked.with_realized_pnl_opt(Some(pnl("-3")));
assert_eq!(retracked.realized_pnl(), Some(pnl("-3")));
}
#[test]
fn with_realized_pnl_force_sets_absolute_value() {
let slot = Holdings::new(ps("10"), ps("0")).with_realized_pnl(pnl("7"));
assert_eq!(
slot.with_realized_pnl(pnl("-3")).realized_pnl(),
Some(pnl("-3"))
);
let with_avg = slot.with_avg_entry_price(Some(px("100")));
let forced = with_avg.with_realized_pnl(pnl("99"));
assert_eq!(forced.realized_pnl(), Some(pnl("99")));
assert_eq!(forced.avg_entry_price(), Some(px("100")));
assert_eq!(forced.available(), ps("10"));
}
#[test]
fn quantity_adjustments_preserve_avg_and_pnl() {
let base = Holdings::new(ps("10"), PositionSize::ZERO)
.with_avg_entry_price(Some(px("100")))
.with_realized_pnl(pnl("7"));
let adjusted = base
.apply_adjustment(
AdjustmentTarget::Available,
AdjustmentAmount::Delta(ps("3")),
)
.expect("must adjust");
assert_eq!(adjusted.available(), ps("13"));
assert_eq!(adjusted.avg_entry_price(), Some(px("100")));
assert_eq!(adjusted.realized_pnl(), Some(pnl("7")));
}
#[test]
fn realize_tracked_pnl_same_side_fill_without_avg_stays_basis_less() {
let slot = Holdings::new(ps("10"), PositionSize::ZERO).with_realized_pnl(pnl("30"));
assert_eq!(slot.avg_entry_price(), None);
let (updated, delta) = slot
.realize_position_fill(ps("5"), px("200"))
.expect("must not overflow");
assert_eq!(delta, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), None);
assert_eq!(updated.realized_pnl(), Some(pnl("30")));
}
#[test]
fn realize_tracked_pnl_opposite_side_fill_without_avg_stays_basis_less() {
let slot = Holdings::new(ps("10"), PositionSize::ZERO).with_realized_pnl(pnl("30"));
let (updated, delta) = slot
.realize_position_fill(ps("-4"), px("150"))
.expect("must not overflow");
assert_eq!(delta, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), None);
assert_eq!(updated.realized_pnl(), Some(pnl("30")));
}
#[test]
fn realize_tracked_pnl_flip_without_avg_seeds_avg_at_price() {
let slot = Holdings::new(ps("5"), PositionSize::ZERO).with_realized_pnl(pnl("30"));
let (updated, delta) = slot
.realize_position_fill(ps("-10"), px("150"))
.expect("must not overflow");
assert_eq!(delta, Some(Pnl::ZERO));
assert_eq!(updated.avg_entry_price(), Some(px("150")));
assert_eq!(updated.realized_pnl(), Some(pnl("30")));
}
}