use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use crate::core::HasAccountId;
use crate::param::{AccountId, Asset, Price, Quantity, TradeAmount, Volume};
use crate::pretrade::policy::{missing_required_field_reject, PolicyGroupId, PolicyName};
use crate::pretrade::DEFAULT_POLICY_GROUP_ID;
use crate::pretrade::{
ConfigurablePolicy, PreTradeContext, PreTradePolicy, Reject, RejectCode, RejectScope, Rejects,
};
use crate::storage::ConfigCell;
use crate::HasInstrument;
use crate::{HasOrderPrice, HasTradeAmount};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OrderSizeLimit {
pub max_quantity: Option<Quantity>,
pub max_notional: Option<Volume>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderSizeBrokerBarrier {
pub limit: OrderSizeLimit,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderSizeAssetBarrier {
pub limit: OrderSizeLimit,
pub asset: Asset,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderSizeAccountAssetBarrier {
pub limit: OrderSizeLimit,
pub account_id: AccountId,
pub asset: Asset,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderSizeLimitPolicyError {
NoBarriersConfigured,
NoCapsConfigured,
DuplicateAssetBarrier {
asset: Asset,
},
DuplicateAccountAssetBarrier {
asset: Asset,
},
}
impl Display for OrderSizeLimitPolicyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoBarriersConfigured => write!(
f,
"at least one broker, asset, or account+asset barrier \
must be configured"
),
Self::NoCapsConfigured => write!(
f,
"at least one of max_quantity or max_notional \
must be configured"
),
Self::DuplicateAssetBarrier { asset } => {
write!(f, "duplicate asset barrier for asset {asset}")
}
Self::DuplicateAccountAssetBarrier { asset } => {
write!(f, "duplicate account+asset barrier for asset {asset}")
}
}
}
}
impl std::error::Error for OrderSizeLimitPolicyError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderSizeLimitSettings {
account_asset_limits: HashMap<(AccountId, Asset), OrderSizeLimit>,
asset_limits: HashMap<Asset, OrderSizeLimit>,
broker: Option<OrderSizeBrokerBarrier>,
}
impl OrderSizeLimitSettings {
fn validate_limit(limit: &OrderSizeLimit) -> Result<(), OrderSizeLimitPolicyError> {
if limit.max_quantity.is_none() && limit.max_notional.is_none() {
return Err(OrderSizeLimitPolicyError::NoCapsConfigured);
}
Ok(())
}
fn validate(
broker: &Option<OrderSizeBrokerBarrier>,
asset_limits: &HashMap<Asset, OrderSizeLimit>,
account_asset_limits: &HashMap<(AccountId, Asset), OrderSizeLimit>,
) -> Result<(), OrderSizeLimitPolicyError> {
if broker.is_none() && asset_limits.is_empty() && account_asset_limits.is_empty() {
return Err(OrderSizeLimitPolicyError::NoBarriersConfigured);
}
Ok(())
}
pub fn new(
broker: Option<OrderSizeBrokerBarrier>,
asset_barriers: impl IntoIterator<Item = OrderSizeAssetBarrier>,
account_asset_barriers: impl IntoIterator<Item = OrderSizeAccountAssetBarrier>,
) -> Result<Self, OrderSizeLimitPolicyError> {
if let Some(barrier) = &broker {
Self::validate_limit(&barrier.limit)?;
}
let mut asset_limits = HashMap::new();
for barrier in asset_barriers {
Self::validate_limit(&barrier.limit)?;
match asset_limits.entry(barrier.asset) {
Entry::Vacant(entry) => {
entry.insert(barrier.limit);
}
Entry::Occupied(entry) => {
return Err(OrderSizeLimitPolicyError::DuplicateAssetBarrier {
asset: entry.key().clone(),
});
}
}
}
let mut account_asset_limits = HashMap::new();
for barrier in account_asset_barriers {
Self::validate_limit(&barrier.limit)?;
match account_asset_limits.entry((barrier.account_id, barrier.asset)) {
Entry::Vacant(entry) => {
entry.insert(barrier.limit);
}
Entry::Occupied(entry) => {
return Err(OrderSizeLimitPolicyError::DuplicateAccountAssetBarrier {
asset: entry.key().1.clone(),
});
}
}
}
Self::validate(&broker, &asset_limits, &account_asset_limits)?;
Ok(Self {
account_asset_limits,
asset_limits,
broker,
})
}
pub fn set_broker(
&mut self,
broker: Option<OrderSizeBrokerBarrier>,
) -> Result<(), OrderSizeLimitPolicyError> {
if let Some(barrier) = &broker {
Self::validate_limit(&barrier.limit)?;
}
Self::validate(&broker, &self.asset_limits, &self.account_asset_limits)?;
self.broker = broker;
Ok(())
}
pub fn set_asset_barriers(
&mut self,
barriers: impl IntoIterator<Item = OrderSizeAssetBarrier>,
) -> Result<(), OrderSizeLimitPolicyError> {
let mut asset_limits = HashMap::new();
for barrier in barriers {
Self::validate_limit(&barrier.limit)?;
match asset_limits.entry(barrier.asset) {
Entry::Vacant(entry) => {
entry.insert(barrier.limit);
}
Entry::Occupied(entry) => {
return Err(OrderSizeLimitPolicyError::DuplicateAssetBarrier {
asset: entry.key().clone(),
});
}
}
}
Self::validate(&self.broker, &asset_limits, &self.account_asset_limits)?;
self.asset_limits = asset_limits;
Ok(())
}
pub fn set_account_asset_barriers(
&mut self,
barriers: impl IntoIterator<Item = OrderSizeAccountAssetBarrier>,
) -> Result<(), OrderSizeLimitPolicyError> {
let mut account_asset_limits = HashMap::new();
for barrier in barriers {
Self::validate_limit(&barrier.limit)?;
match account_asset_limits.entry((barrier.account_id, barrier.asset)) {
Entry::Vacant(entry) => {
entry.insert(barrier.limit);
}
Entry::Occupied(entry) => {
return Err(OrderSizeLimitPolicyError::DuplicateAccountAssetBarrier {
asset: entry.key().1.clone(),
});
}
}
}
Self::validate(&self.broker, &self.asset_limits, &account_asset_limits)?;
self.account_asset_limits = account_asset_limits;
Ok(())
}
}
pub struct OrderSizeLimitPolicy<LockingPolicyFactory>
where
LockingPolicyFactory: crate::storage::LockingPolicyFactory,
{
group_id: PolicyGroupId,
settings: <LockingPolicyFactory as crate::storage::LockingPolicyFactory>::Config<
OrderSizeLimitSettings,
>,
}
impl<LockingPolicyFactory> OrderSizeLimitPolicy<LockingPolicyFactory>
where
LockingPolicyFactory: crate::storage::LockingPolicyFactory,
{
pub const NAME: &'static str = "OrderSizeLimitPolicy";
pub fn new(settings: OrderSizeLimitSettings) -> Self {
Self {
group_id: DEFAULT_POLICY_GROUP_ID,
settings: <LockingPolicyFactory as crate::storage::LockingPolicyFactory>::new_config(
settings,
),
}
}
pub fn with_policy_group_id(mut self, id: PolicyGroupId) -> Self {
self.group_id = id;
self
}
}
impl<LockingPolicyFactory> PolicyName for OrderSizeLimitPolicy<LockingPolicyFactory>
where
LockingPolicyFactory: crate::storage::LockingPolicyFactory,
{
fn policy_name(&self) -> &str {
Self::NAME
}
}
impl<LockingPolicyFactory, Order, ExecutionReport, AccountAdjustment, Sync>
PreTradePolicy<Order, ExecutionReport, AccountAdjustment, Sync>
for OrderSizeLimitPolicy<LockingPolicyFactory>
where
LockingPolicyFactory:
crate::storage::LockingPolicyFactory + crate::storage::CreateStorageFor<AccountId>,
Order: HasInstrument + HasTradeAmount + HasOrderPrice + HasAccountId,
Sync: crate::core::SyncMode<StorageLockingPolicyFactory = LockingPolicyFactory>,
{
fn name(&self) -> &str {
Self::NAME
}
fn policy_group_id(&self) -> PolicyGroupId {
self.group_id
}
#[allow(private_interfaces)]
fn built_in_config_entry(&self) -> Option<crate::core::ConfigEntry<LockingPolicyFactory>> {
Some(crate::core::ConfigEntry::OrderSizeLimit(
crate::pretrade::ConfigurablePolicy::settings_cell(self),
))
}
fn check_pre_trade_start(
&self,
ctx: &PreTradeContext<<Sync as crate::core::SyncMode>::StorageLockingPolicyFactory>,
order: &Order,
) -> Result<(), Rejects> {
if ctx.is_drop_copy() {
return Ok(());
}
self.settings.with_snapshot(|settings| {
let needs_instrument =
!settings.asset_limits.is_empty() || !settings.account_asset_limits.is_empty();
let instrument = if needs_instrument {
Some(order.instrument().map_err(|e| {
Rejects::from(missing_required_field_reject(self, "instrument", &e))
})?)
} else {
None
};
let needs_account = !settings.account_asset_limits.is_empty();
let account_id = if needs_account {
Some(order.account_id().map_err(|e| {
Rejects::from(missing_required_field_reject(self, "account ID", &e))
})?)
} else {
None
};
let quantity_axis_limit = instrument.and_then(|instrument| {
select_asset_limit(
settings,
account_id,
instrument.underlying_asset(),
|limit| limit.max_quantity,
)
});
let notional_axis_limit = instrument.and_then(|instrument| {
select_asset_limit(
settings,
account_id,
instrument.settlement_asset(),
|limit| limit.max_notional,
)
});
let broker_limit = settings.broker.as_ref().map(|barrier| barrier.limit);
let quantity_broker_limit = broker_limit
.and_then(|limit| limit.max_quantity)
.map(|maximum| SelectedMetricLimit { maximum });
let notional_broker_limit = broker_limit
.and_then(|limit| limit.max_notional)
.map(|maximum| SelectedMetricLimit { maximum });
let quantity_needed = quantity_axis_limit.is_some() || quantity_broker_limit.is_some();
let notional_needed = notional_axis_limit.is_some() || notional_broker_limit.is_some();
if !quantity_needed && !notional_needed {
return Ok(());
}
let trade_amount = order.trade_amount().map_err(|e| {
Rejects::from(missing_required_field_reject(self, "trade amount", &e))
})?;
let quantity_needs_price =
matches!(trade_amount, TradeAmount::Volume(_)) && quantity_needed;
let notional_needs_price =
matches!(trade_amount, TradeAmount::Quantity(_)) && notional_needed;
let price = if quantity_needs_price || notional_needs_price {
order.price().map_err(|error| {
Rejects::from(missing_required_field_reject(self, "price", &error))
})?
} else {
None
};
let (quantity_axis, quantity_broker) =
resolve_metric_limits(quantity_axis_limit, quantity_broker_limit, || {
resolve_quantity(Self::NAME, trade_amount, price)
})
.map_err(Rejects::from)?;
let (notional_axis, notional_broker) =
resolve_metric_limits(notional_axis_limit, notional_broker_limit, || {
resolve_notional(Self::NAME, trade_amount, price)
})
.map_err(Rejects::from)?;
let axis_reject = check_limit_optional(
Self::NAME,
quantity_axis,
notional_axis,
instrument.map(|instrument| {
(instrument.underlying_asset(), instrument.settlement_asset())
}),
);
let broker_reject =
check_limit_optional(Self::NAME, quantity_broker, notional_broker, None);
if let Some(reject) = axis_reject.or(broker_reject) {
return Err(Rejects::from(reject));
}
Ok(())
})
}
}
impl<LockingPolicyFactory> ConfigurablePolicy<LockingPolicyFactory>
for OrderSizeLimitPolicy<LockingPolicyFactory>
where
LockingPolicyFactory: crate::storage::LockingPolicyFactory,
{
type Settings = OrderSizeLimitSettings;
fn settings_cell(
&self,
) -> <LockingPolicyFactory as crate::storage::LockingPolicyFactory>::Config<
OrderSizeLimitSettings,
> {
self.settings.clone()
}
}
struct SelectedMetricLimit<MetricValue> {
maximum: MetricValue,
}
impl<MetricValue> SelectedMetricLimit<MetricValue> {
fn resolve(self, requested: MetricValue) -> ResolvedMetricLimit<MetricValue> {
ResolvedMetricLimit {
requested,
maximum: self.maximum,
}
}
}
struct ResolvedMetricLimit<MetricValue> {
requested: MetricValue,
maximum: MetricValue,
}
fn select_asset_limit<MetricValue: Copy>(
settings: &OrderSizeLimitSettings,
account_id: Option<AccountId>,
asset: &Asset,
metric: impl Fn(&OrderSizeLimit) -> Option<MetricValue>,
) -> Option<SelectedMetricLimit<MetricValue>> {
if let Some(account_id) = account_id {
if let Some(limit) = settings
.account_asset_limits
.get(&(account_id, asset.clone()))
{
if let Some(maximum) = metric(limit) {
return Some(SelectedMetricLimit { maximum });
}
}
}
settings
.asset_limits
.get(asset)
.and_then(|limit| metric(limit).map(|maximum| SelectedMetricLimit { maximum }))
}
type ResolvedMetricPair<MetricValue> = (
Option<ResolvedMetricLimit<MetricValue>>,
Option<ResolvedMetricLimit<MetricValue>>,
);
fn resolve_metric_limits<MetricValue: Copy>(
axis_limit: Option<SelectedMetricLimit<MetricValue>>,
broker_limit: Option<SelectedMetricLimit<MetricValue>>,
resolve: impl FnOnce() -> Result<MetricValue, Reject>,
) -> Result<ResolvedMetricPair<MetricValue>, Reject> {
if axis_limit.is_none() && broker_limit.is_none() {
return Ok((None, None));
}
let requested = resolve()?;
Ok((
axis_limit.map(|limit| limit.resolve(requested)),
broker_limit.map(|limit| limit.resolve(requested)),
))
}
fn check_limit_optional(
policy: &str,
quantity: Option<ResolvedMetricLimit<Quantity>>,
notional: Option<ResolvedMetricLimit<Volume>>,
assets: Option<(&Asset, &Asset)>,
) -> Option<Reject> {
let quantity = quantity.filter(|limit| limit.requested > limit.maximum);
let notional = notional.filter(|limit| limit.requested > limit.maximum);
match (quantity, notional) {
(None, None) => None,
(Some(quantity), None) => Some(Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderQtyExceedsLimit,
"order quantity exceeded",
format!(
"requested {}, max allowed: {}",
quantity.requested, quantity.maximum
),
)),
(None, Some(notional)) => Some(Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderNotionalExceedsLimit,
"order notional exceeded",
format!(
"requested {}, max allowed: {}",
notional.requested, notional.maximum
),
)),
(Some(quantity), Some(notional)) => {
let details = match assets {
Some((quantity_asset, notional_asset)) => format!(
"requested quantity {} for asset {quantity_asset}, \
max allowed: {}; requested notional {} for asset \
{notional_asset}, max allowed: {}",
quantity.requested, quantity.maximum, notional.requested, notional.maximum
),
None => format!(
"requested quantity {}, max allowed: {}; \
requested notional {}, max allowed: {}",
quantity.requested, quantity.maximum, notional.requested, notional.maximum
),
};
Some(Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderExceedsLimit,
"order size exceeded",
details,
))
}
}
}
fn resolve_notional(
policy: &str,
trade_amount: TradeAmount,
price: Option<Price>,
) -> Result<Volume, Reject> {
match (trade_amount, price) {
(TradeAmount::Volume(volume), _) => Ok(volume),
(TradeAmount::Quantity(quantity), Some(price)) => {
price.calculate_volume(quantity).map_err(|_| {
order_value_calculation_failed_reject(
policy,
"price or quantity could not be used to evaluate order notional",
)
})
}
(TradeAmount::Quantity(_), None) => Err(order_value_calculation_failed_reject(
policy,
"price not provided for evaluating cash flow/notional/volume",
)),
}
}
fn resolve_quantity(
policy: &str,
trade_amount: TradeAmount,
price: Option<Price>,
) -> Result<Quantity, Reject> {
match (trade_amount, price) {
(TradeAmount::Quantity(quantity), _) => Ok(quantity),
(TradeAmount::Volume(volume), Some(price)) => {
volume.calculate_quantity(price).map_err(|_| {
order_value_calculation_failed_reject(
policy,
"price or volume could not be used to evaluate order quantity",
)
})
}
(TradeAmount::Volume(_), None) => Err(order_value_calculation_failed_reject(
policy,
"price not provided for evaluating cash flow/notional/volume",
)),
}
}
fn order_value_calculation_failed_reject(policy: &str, details: &'static str) -> Reject {
Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderValueCalculationFailed,
"order value calculation failed",
details,
)
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use crate::core::{HasAccountId, Instrument, OrderOperation};
use crate::param::TradeAmount;
use crate::param::{AccountId, Asset, Price, Quantity, Side, Volume};
use crate::pretrade::{PreTradeContext, PreTradePolicy, RejectCode, RejectScope};
use crate::storage::{ConfigCell, LocalConfigCell, NoLocking};
use crate::{Engine, HasInstrument, HasOrderPrice, HasTradeAmount, RequestFieldAccessError};
use rust_decimal::Decimal;
use super::{
OrderSizeAccountAssetBarrier, OrderSizeAssetBarrier, OrderSizeBrokerBarrier,
OrderSizeLimit, OrderSizeLimitPolicy, OrderSizeLimitPolicyError, OrderSizeLimitSettings,
};
type TestPolicy = OrderSizeLimitPolicy<NoLocking>;
type TestOrder = OrderOperation;
struct InstrumentAccessErrorOrder;
struct AccountAccessErrorOrder {
instrument: Instrument,
trade_amount_access_count: Cell<usize>,
trade_amount_access_error: bool,
}
#[derive(Clone)]
struct AccountAccessCountingOrder {
instrument: Instrument,
account_id: AccountId,
instrument_access_count: Rc<Cell<usize>>,
account_id_access_count: Rc<Cell<usize>>,
trade_amount_access_count: Rc<Cell<usize>>,
price_access_count: Rc<Cell<usize>>,
}
impl HasInstrument for InstrumentAccessErrorOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("instrument"))
}
}
impl HasAccountId for InstrumentAccessErrorOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("account_id"))
}
}
impl HasTradeAmount for InstrumentAccessErrorOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
Ok(TradeAmount::Quantity(
Quantity::from_str("1").expect("quantity literal must be valid"),
))
}
}
impl HasOrderPrice for InstrumentAccessErrorOrder {
fn price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(Some(
Price::from_str("1").expect("price literal must be valid"),
))
}
}
impl HasInstrument for AccountAccessCountingOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
self.instrument_access_count
.set(self.instrument_access_count.get() + 1);
Ok(&self.instrument)
}
}
impl HasAccountId for AccountAccessCountingOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
self.account_id_access_count
.set(self.account_id_access_count.get() + 1);
Ok(self.account_id)
}
}
impl HasTradeAmount for AccountAccessCountingOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
self.trade_amount_access_count
.set(self.trade_amount_access_count.get() + 1);
Ok(TradeAmount::Quantity(
Quantity::from_str("1").expect("quantity literal must be valid"),
))
}
}
impl HasOrderPrice for AccountAccessCountingOrder {
fn price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
self.price_access_count
.set(self.price_access_count.get() + 1);
Ok(None)
}
}
impl HasInstrument for AccountAccessErrorOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
impl HasAccountId for AccountAccessErrorOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("account_id"))
}
}
impl HasTradeAmount for AccountAccessErrorOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
self.trade_amount_access_count
.set(self.trade_amount_access_count.get() + 1);
if self.trade_amount_access_error {
Err(RequestFieldAccessError::new("trade_amount"))
} else {
Ok(TradeAmount::Quantity(
Quantity::from_str("1").expect("quantity literal must be valid"),
))
}
}
}
impl HasOrderPrice for AccountAccessErrorOrder {
fn price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(Some(
Price::from_str("1").expect("price literal must be valid"),
))
}
}
fn order(settlement: &str, quantity: &str, price: &str) -> TestOrder {
order_for_account(settlement, quantity, price, AccountId::from_u64(99224416))
}
fn order_for_account(
settlement: &str,
quantity: &str,
price: &str,
account_id: AccountId,
) -> TestOrder {
order_for_instrument("AAPL", settlement, quantity, price, account_id)
}
fn order_for_instrument(
underlying: &str,
settlement: &str,
quantity: &str,
price: &str,
account_id: AccountId,
) -> TestOrder {
OrderOperation {
instrument: Instrument::new(
Asset::new(underlying).expect("asset code must be valid"),
Asset::new(settlement).expect("asset code must be valid"),
),
account_id,
side: Side::Buy,
trade_amount: TradeAmount::Quantity(
Quantity::from_str(quantity).expect("quantity literal must be valid"),
),
price: Some(Price::from_str(price).expect("price literal must be valid")),
}
}
fn limit(max_quantity: &str, max_notional: &str) -> OrderSizeLimit {
OrderSizeLimit {
max_quantity: Some(
Quantity::from_str(max_quantity).expect("max quantity literal must be valid"),
),
max_notional: Some(
Volume::from_str(max_notional).expect("max notional literal must be valid"),
),
}
}
fn quantity_limit(max_quantity: &str) -> OrderSizeLimit {
OrderSizeLimit {
max_quantity: Some(
Quantity::from_str(max_quantity).expect("max quantity literal must be valid"),
),
max_notional: None,
}
}
fn notional_limit(max_notional: &str) -> OrderSizeLimit {
OrderSizeLimit {
max_quantity: None,
max_notional: Some(
Volume::from_str(max_notional).expect("max notional literal must be valid"),
),
}
}
fn empty_limit() -> OrderSizeLimit {
OrderSizeLimit {
max_quantity: None,
max_notional: None,
}
}
fn asset_barrier(asset: &str, max_quantity: &str, max_notional: &str) -> OrderSizeAssetBarrier {
OrderSizeAssetBarrier {
limit: limit(max_quantity, max_notional),
asset: Asset::new(asset).expect("asset code must be valid"),
}
}
fn asset_quantity_barrier(asset: &str, max_quantity: &str) -> OrderSizeAssetBarrier {
OrderSizeAssetBarrier {
limit: quantity_limit(max_quantity),
asset: Asset::new(asset).expect("asset code must be valid"),
}
}
fn asset_notional_barrier(asset: &str, max_notional: &str) -> OrderSizeAssetBarrier {
OrderSizeAssetBarrier {
limit: notional_limit(max_notional),
asset: Asset::new(asset).expect("asset code must be valid"),
}
}
fn broker_barrier(max_quantity: &str, max_notional: &str) -> OrderSizeBrokerBarrier {
OrderSizeBrokerBarrier {
limit: limit(max_quantity, max_notional),
}
}
fn broker_quantity_barrier(max_quantity: &str) -> OrderSizeBrokerBarrier {
OrderSizeBrokerBarrier {
limit: quantity_limit(max_quantity),
}
}
fn settings(
broker: Option<OrderSizeBrokerBarrier>,
asset_barriers: impl IntoIterator<Item = OrderSizeAssetBarrier>,
account_asset_barriers: impl IntoIterator<Item = OrderSizeAccountAssetBarrier>,
) -> OrderSizeLimitSettings {
OrderSizeLimitSettings::new(broker, asset_barriers, account_asset_barriers)
.expect("settings must be valid in helper")
}
fn policy(
broker: Option<OrderSizeBrokerBarrier>,
asset_barriers: impl IntoIterator<Item = OrderSizeAssetBarrier>,
account_asset_barriers: impl IntoIterator<Item = OrderSizeAccountAssetBarrier>,
) -> TestPolicy {
TestPolicy::new(settings(broker, asset_barriers, account_asset_barriers))
}
fn check(p: &TestPolicy, order: &TestOrder) -> Result<(), crate::pretrade::Rejects> {
<TestPolicy as PreTradePolicy<TestOrder, (), (), crate::core::LocalSync>>::check_pre_trade_start(
p,
&PreTradeContext::<NoLocking>::new(None),
order,
)
}
#[test]
fn no_barriers_configured_rejected_by_settings_constructor() {
let err = OrderSizeLimitSettings::new(None, [], []).expect_err("must fail");
assert_eq!(err, OrderSizeLimitPolicyError::NoBarriersConfigured);
assert_eq!(
err.to_string(),
"at least one broker, asset, or account+asset barrier \
must be configured"
);
}
#[test]
fn empty_limit_rejected_by_settings_constructor() {
let err = OrderSizeLimitSettings::new(
None,
[
OrderSizeAssetBarrier {
limit: empty_limit(),
asset: Asset::new("AAPL").expect("asset code must be valid"),
},
asset_quantity_barrier("AAPL", "10"),
],
[],
)
.expect_err("an empty duplicate must not be discarded");
assert_eq!(err, OrderSizeLimitPolicyError::NoCapsConfigured);
assert_eq!(
err.to_string(),
"at least one of max_quantity or max_notional \
must be configured"
);
}
#[test]
fn duplicate_asset_key_rejected_by_settings_constructor() {
let asset = Asset::new("AAPL").expect("asset code must be valid");
let err = OrderSizeLimitSettings::new(
None,
[
OrderSizeAssetBarrier {
limit: quantity_limit("10"),
asset: asset.clone(),
},
OrderSizeAssetBarrier {
limit: notional_limit("500000"),
asset: asset.clone(),
},
],
[],
)
.expect_err("duplicate asset key must fail");
assert_eq!(
err,
OrderSizeLimitPolicyError::DuplicateAssetBarrier {
asset: asset.clone()
}
);
assert_eq!(err.to_string(), "duplicate asset barrier for asset AAPL");
}
#[test]
fn duplicate_asset_key_rejected_by_runtime_setter() {
let mut s = settings(Some(broker_quantity_barrier("100")), [], []);
let original = s.clone();
let asset = Asset::new("AAPL").expect("asset code must be valid");
let err = s
.set_asset_barriers([
OrderSizeAssetBarrier {
limit: quantity_limit("10"),
asset: asset.clone(),
},
OrderSizeAssetBarrier {
limit: notional_limit("500000"),
asset: asset.clone(),
},
])
.expect_err("duplicate asset key must fail");
assert_eq!(
err,
OrderSizeLimitPolicyError::DuplicateAssetBarrier { asset }
);
assert_eq!(s, original);
}
#[test]
fn duplicate_account_asset_key_rejected_by_settings_constructor() {
let account_id = AccountId::from_u64(99224416);
let asset = Asset::new("AAPL").expect("asset code must be valid");
let err = OrderSizeLimitSettings::new(
None,
[],
[
OrderSizeAccountAssetBarrier {
limit: quantity_limit("10"),
account_id,
asset: asset.clone(),
},
OrderSizeAccountAssetBarrier {
limit: notional_limit("500000"),
account_id,
asset: asset.clone(),
},
],
)
.expect_err("duplicate account+asset key must fail");
assert_eq!(
err,
OrderSizeLimitPolicyError::DuplicateAccountAssetBarrier {
asset: asset.clone()
}
);
assert_eq!(
err.to_string(),
"duplicate account+asset barrier for asset AAPL"
);
}
#[test]
fn duplicate_account_asset_key_rejected_by_runtime_setter() {
let mut s = settings(Some(broker_quantity_barrier("100")), [], []);
let original = s.clone();
let account_id = AccountId::from_u64(99224416);
let asset = Asset::new("AAPL").expect("asset code must be valid");
let err = s
.set_account_asset_barriers([
OrderSizeAccountAssetBarrier {
limit: quantity_limit("10"),
account_id,
asset: asset.clone(),
},
OrderSizeAccountAssetBarrier {
limit: notional_limit("500000"),
account_id,
asset: asset.clone(),
},
])
.expect_err("duplicate account+asset key must fail");
assert_eq!(
err,
OrderSizeLimitPolicyError::DuplicateAccountAssetBarrier { asset }
);
assert_eq!(s, original);
}
#[test]
fn distinct_account_asset_keys_are_accepted() {
let first_account = AccountId::from_u64(1);
let second_account = AccountId::from_u64(2);
let aapl = Asset::new("AAPL").expect("asset code must be valid");
let usd = Asset::new("USD").expect("asset code must be valid");
let s = OrderSizeLimitSettings::new(
None,
[],
[
OrderSizeAccountAssetBarrier {
limit: quantity_limit("10"),
account_id: first_account,
asset: aapl.clone(),
},
OrderSizeAccountAssetBarrier {
limit: notional_limit("500000"),
account_id: second_account,
asset: aapl,
},
OrderSizeAccountAssetBarrier {
limit: quantity_limit("20"),
account_id: first_account,
asset: usd,
},
],
)
.expect(
"same asset under another account and another asset under the same account are valid",
);
assert_eq!(s.account_asset_limits.len(), 3);
}
#[test]
fn empty_limit_rejected_by_runtime_setters() {
let mut s = settings(None, [asset_quantity_barrier("AAPL", "10")], []);
let empty_asset = || OrderSizeAssetBarrier {
limit: empty_limit(),
asset: Asset::new("AAPL").expect("asset code must be valid"),
};
let empty_account_asset = || OrderSizeAccountAssetBarrier {
limit: empty_limit(),
account_id: AccountId::from_u64(99224416),
asset: Asset::new("AAPL").expect("asset code must be valid"),
};
let err = s
.set_asset_barriers([empty_asset()])
.expect_err("asset limit without caps must fail");
assert_eq!(err, OrderSizeLimitPolicyError::NoCapsConfigured);
let err = s
.set_account_asset_barriers([empty_account_asset()])
.expect_err("account+asset limit without caps must fail");
assert_eq!(err, OrderSizeLimitPolicyError::NoCapsConfigured);
let err = s
.set_broker(Some(OrderSizeBrokerBarrier {
limit: empty_limit(),
}))
.expect_err("broker limit without caps must fail");
assert_eq!(err, OrderSizeLimitPolicyError::NoCapsConfigured);
}
#[test]
fn set_broker_to_none_rejected_when_other_axes_empty() {
let mut s = settings(Some(broker_barrier("5", "500")), [], []);
let err = s.set_broker(None).expect_err("must fail");
assert_eq!(err, OrderSizeLimitPolicyError::NoBarriersConfigured);
assert!(s.broker.is_some());
}
#[test]
fn set_asset_barriers_to_empty_rejected_when_other_axes_empty() {
let mut s = settings(None, [asset_barrier("USD", "10", "1000")], []);
let err = s.set_asset_barriers([]).expect_err("must fail");
assert_eq!(err, OrderSizeLimitPolicyError::NoBarriersConfigured);
}
#[test]
fn no_barriers_configured_rejected_by_constructor() {
let err = OrderSizeLimitSettings::new(None, [], []).expect_err("must fail");
assert_eq!(err, OrderSizeLimitPolicyError::NoBarriersConfigured);
}
#[test]
fn quantity_violation_returns_order_quantity_exceeded() {
let p = policy(None, [asset_quantity_barrier("AAPL", "10")], []);
assert!(check(&p, &order("USD", "10", "1000000")).is_ok());
let reject = check(&p, &order("USD", "11", "90")).expect_err("quantity must be rejected");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::OrderQtyExceedsLimit);
assert_eq!(reject.reason, "order quantity exceeded");
assert_eq!(reject.details, "requested 11, max allowed: 10");
let other_instrument =
order_for_instrument("MSFT", "USD", "11", "90", AccountId::from_u64(99224416));
assert!(check(&p, &other_instrument).is_ok());
}
#[test]
fn notional_violation_returns_order_notional_exceeded() {
let p = policy(None, [asset_notional_barrier("USD", "1000")], []);
assert!(check(&p, &order("USD", "1000", "1")).is_ok());
let reject = check(&p, &order("USD", "10", "101")).expect_err("notional must be rejected");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::OrderNotionalExceedsLimit);
assert_eq!(reject.reason, "order notional exceeded");
assert_eq!(reject.details, "requested 1010, max allowed: 1000");
let other_instrument =
order_for_instrument("MSFT", "USD", "10", "101", AccountId::from_u64(99224416));
let reject = check(&p, &other_instrument)
.expect_err("USD notional limit must apply to another instrument");
assert_eq!(reject[0].code, RejectCode::OrderNotionalExceedsLimit);
}
#[test]
fn both_violations_are_returned_in_single_reject() {
let p = policy(
None,
[
asset_quantity_barrier("AAPL", "10"),
asset_notional_barrier("USD", "1000"),
],
[],
);
let reject = check(&p, &order("USD", "11", "100"))
.expect_err("quantity and notional must be rejected");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::OrderExceedsLimit);
assert_eq!(reject.reason, "order size exceeded");
assert_eq!(
reject.details,
"requested quantity 11 for asset AAPL, max allowed: 10; \
requested notional 1100 for asset USD, max allowed: 1000"
);
}
#[test]
fn no_applicable_limit_passes_silently() {
let p = policy(None, [asset_barrier("EUR", "10", "1000")], []);
assert!(check(&p, &order("USD", "1", "1")).is_ok());
}
#[test]
fn boundary_values_are_accepted() {
let p = policy(
None,
[
asset_quantity_barrier("AAPL", "10"),
asset_notional_barrier("USD", "1000"),
],
[],
);
assert!(check(&p, &order("USD", "10", "100")).is_ok());
}
#[test]
fn irrelevant_metric_does_not_require_price_conversion() {
let quantity_policy = policy(None, [asset_quantity_barrier("AAPL", "10")], []);
let quantity_order = OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
trade_amount: TradeAmount::Quantity(
Quantity::from_str("10").expect("quantity literal must be valid"),
),
price: None,
};
assert!(check(&quantity_policy, &quantity_order).is_ok());
let notional_policy = policy(None, [asset_notional_barrier("USD", "100")], []);
let notional_order = OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
trade_amount: TradeAmount::Volume(
Volume::from_str("100").expect("volume literal must be valid"),
),
price: None,
};
assert!(check(¬ional_policy, ¬ional_order).is_ok());
}
#[test]
fn broker_barrier_applies_regardless_of_settlement() {
let p = policy(Some(broker_barrier("5", "500")), [], []);
let reject = check(&p, &order("USD", "6", "10")).expect_err("broker barrier must reject");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderQtyExceedsLimit);
let reject2 =
check(&p, &order("EUR", "6", "10")).expect_err("broker barrier applies to EUR too");
assert_eq!(reject2[0].scope, RejectScope::Order);
}
#[test]
fn broker_combined_reject_details_remain_asset_agnostic() {
let p = policy(Some(broker_barrier("5", "500")), [], []);
let reject =
check(&p, &order("USD", "6", "100")).expect_err("both broker caps must reject");
assert_eq!(reject[0].code, RejectCode::OrderExceedsLimit);
assert_eq!(
reject[0].details,
"requested quantity 6, max allowed: 5; \
requested notional 600, max allowed: 500"
);
}
#[test]
fn account_asset_barrier_overrides_asset_barrier() {
let p = policy(
None,
[asset_quantity_barrier("AAPL", "10")],
[OrderSizeAccountAssetBarrier {
limit: quantity_limit("5"),
account_id: AccountId::from_u64(99224416),
asset: Asset::new("AAPL").unwrap(),
}],
);
let reject = check(&p, &order("USD", "6", "10"))
.expect_err("account+asset barrier (max 5) must override asset barrier (max 10)");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderQtyExceedsLimit);
}
#[test]
fn account_asset_barrier_with_looser_limit_overrides_asset_baseline() {
let p = policy(
None,
[asset_quantity_barrier("AAPL", "5")],
[OrderSizeAccountAssetBarrier {
limit: quantity_limit("100"),
account_id: AccountId::from_u64(99224416),
asset: Asset::new("AAPL").unwrap(),
}],
);
assert!(check(
&p,
&order_for_account("USD", "10", "10", AccountId::from_u64(99224416))
)
.is_ok());
let reject = check(
&p,
&order_for_account("USD", "10", "10", AccountId::from_u64(2)),
)
.expect_err("asset baseline must reject unmatched account");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderQtyExceedsLimit);
}
#[test]
fn account_barrier_without_quantity_does_not_mask_asset_quantity() {
let account_id = AccountId::from_u64(99224416);
let p = policy(
None,
[asset_quantity_barrier("AAPL", "10")],
[OrderSizeAccountAssetBarrier {
limit: notional_limit("500"),
account_id,
asset: Asset::new("AAPL").expect("asset code must be valid"),
}],
);
let reject = check(&p, &order_for_account("USD", "11", "1", account_id))
.expect_err("asset quantity cap must survive the account-level gap");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderQtyExceedsLimit);
assert_eq!(reject[0].details, "requested 11, max allowed: 10");
}
#[test]
fn account_barrier_without_notional_does_not_mask_asset_notional() {
let account_id = AccountId::from_u64(99224416);
let p = policy(
None,
[asset_notional_barrier("USD", "500")],
[OrderSizeAccountAssetBarrier {
limit: quantity_limit("10"),
account_id,
asset: Asset::new("USD").expect("asset code must be valid"),
}],
);
let reject = check(&p, &order_for_account("USD", "6", "100", account_id))
.expect_err("asset notional cap must survive the account-level gap");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderNotionalExceedsLimit);
assert_eq!(reject[0].details, "requested 600, max allowed: 500");
}
#[test]
fn account_overrides_resolve_independently_for_both_metrics() {
let account_id = AccountId::from_u64(99224416);
let p = policy(
None,
[
asset_quantity_barrier("AAPL", "10"),
asset_notional_barrier("USD", "1000"),
],
[
OrderSizeAccountAssetBarrier {
limit: quantity_limit("5"),
account_id,
asset: Asset::new("AAPL").expect("asset code must be valid"),
},
OrderSizeAccountAssetBarrier {
limit: notional_limit("500"),
account_id,
asset: Asset::new("USD").expect("asset code must be valid"),
},
],
);
let reject = check(&p, &order_for_account("USD", "6", "100", account_id))
.expect_err("both account-specific limits must reject");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderExceedsLimit);
assert_eq!(
reject[0].details,
"requested quantity 6 for asset AAPL, max allowed: 5; \
requested notional 600 for asset USD, max allowed: 500"
);
assert!(check(
&p,
&order_for_account("USD", "6", "100", AccountId::from_u64(2))
)
.is_ok());
}
#[test]
fn combined_reject_stays_order_scoped_across_metrics() {
let account_id = AccountId::from_u64(99224416);
let p = policy(
None,
[asset_quantity_barrier("AAPL", "5")],
[OrderSizeAccountAssetBarrier {
limit: notional_limit("500"),
account_id,
asset: Asset::new("USD").expect("asset code must be valid"),
}],
);
let reject = check(&p, &order_for_account("USD", "6", "100", account_id))
.expect_err("both asset-chain metrics must reject");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderExceedsLimit);
}
#[test]
fn same_asset_barrier_supplies_both_caps() {
let account_id = AccountId::from_u64(99224416);
let p = policy(
None,
[],
[OrderSizeAccountAssetBarrier {
limit: limit("5", "500"),
account_id,
asset: Asset::new("AAPL").expect("asset code must be valid"),
}],
);
let order = order_for_instrument("AAPL", "AAPL", "6", "100", account_id);
let reject = check(&p, &order).expect_err("both caps from one entry must reject");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderExceedsLimit);
assert_eq!(
reject[0].details,
"requested quantity 6 for asset AAPL, max allowed: 5; \
requested notional 600 for asset AAPL, max allowed: 500"
);
}
#[test]
fn unknown_settlement_passes_when_no_broker_or_account_asset_match() {
let p = policy(
Some(broker_barrier("1000", "1000000")),
[asset_barrier("EUR", "10", "1000")],
[OrderSizeAccountAssetBarrier {
limit: limit("5", "500"),
account_id: AccountId::from_u64(99224416),
asset: Asset::new("USD").expect("asset code must be valid"),
}],
);
assert!(check(&p, &order("JPY", "1", "1")).is_ok());
}
#[test]
fn axis_reject_reported_before_broker_reject_when_both_breach() {
let p = policy(
Some(broker_quantity_barrier("5")),
[asset_quantity_barrier("AAPL", "3")],
[],
);
let reject = check(&p, &order("USD", "6", "10")).expect_err("must reject");
assert_eq!(reject[0].scope, RejectScope::Order);
assert_eq!(reject[0].code, RejectCode::OrderQtyExceedsLimit);
assert!(
reject[0].details.contains("max allowed: 3"),
"should report asset barrier limit"
);
}
#[test]
fn additional_asset_barriers_at_construction_are_applied() {
let p = policy(
None,
vec![
asset_notional_barrier("USD", "1000"),
asset_notional_barrier("EUR", "500"),
asset_notional_barrier("GBP", "300"),
],
[],
);
assert!(check(&p, &order("EUR", "5", "100")).is_ok());
assert!(check(&p, &order("GBP", "3", "100")).is_ok());
let reject = check(&p, &order("EUR", "6", "100"))
.expect_err("exceeding EUR notional limit must reject");
assert_eq!(reject[0].code, RejectCode::OrderNotionalExceedsLimit);
assert_eq!(reject[0].details, "requested 600, max allowed: 500");
}
#[test]
fn policy_name_is_stable() {
let p = policy(None, [asset_barrier("AAPL", "10", "1000")], []);
assert_eq!(
<TestPolicy as PreTradePolicy<TestOrder, (), (), crate::core::LocalSync>>::name(&p),
OrderSizeLimitPolicy::<NoLocking>::NAME
);
}
#[test]
fn apply_execution_report_returns_false() {
let p = policy(None, [asset_barrier("USD", "10", "1000")], []);
assert!(<TestPolicy as PreTradePolicy<
TestOrder,
(),
(),
crate::core::LocalSync,
>>::apply_execution_report(
&p, &crate::pretrade::PostTradeContext::new(), &()
)
.is_none());
}
#[test]
fn settings_cell_clone_shares_underlying_value() {
use crate::pretrade::ConfigurablePolicy;
let p = policy(None, [asset_quantity_barrier("AAPL", "10")], []);
let cell = p.settings_cell();
cell.update::<OrderSizeLimitPolicyError>(|s| {
s.set_asset_barriers([asset_quantity_barrier("AAPL", "20")])
})
.expect("update must succeed");
assert!(check(&p, &order("USD", "15", "100")).is_ok());
let reject = check(&p, &order("USD", "21", "100")).expect_err("21 exceeds new limit of 20");
assert_eq!(reject[0].code, RejectCode::OrderQtyExceedsLimit);
assert!(reject[0].details.contains("max allowed: 20"));
}
#[test]
fn price_accessor_can_reconfigure_policy_without_borrow_panic() {
use crate::pretrade::ConfigurablePolicy;
struct ReconfiguringPriceOrder {
settings: LocalConfigCell<OrderSizeLimitSettings>,
instrument: Instrument,
}
impl HasInstrument for ReconfiguringPriceOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
impl HasAccountId for ReconfiguringPriceOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Ok(AccountId::from_u64(99224416))
}
}
impl HasTradeAmount for ReconfiguringPriceOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
Ok(TradeAmount::Quantity(
Quantity::from_str("11").expect("quantity literal must be valid"),
))
}
}
impl HasOrderPrice for ReconfiguringPriceOrder {
fn price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
self.settings
.update::<OrderSizeLimitPolicyError>(|settings| {
settings.set_asset_barriers([asset_notional_barrier("USD", "2000")])
})
.expect("price accessor must reconfigure without a borrow panic");
Ok(Some(
Price::from_str("100").expect("price literal must be valid"),
))
}
}
let p = policy(None, [asset_notional_barrier("USD", "1000")], []);
let order = ReconfiguringPriceOrder {
settings: p.settings_cell(),
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
};
let reject = <TestPolicy as PreTradePolicy<
ReconfiguringPriceOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order
)
.expect_err("the copied pre-update cap must reject");
assert_eq!(reject[0].code, RejectCode::OrderNotionalExceedsLimit);
assert_eq!(reject[0].details, "requested 1100, max allowed: 1000");
assert!(check(
&p,
&order_for_account("USD", "11", "100", AccountId::from_u64(99224416),)
)
.is_ok());
}
#[test]
fn trade_amount_reconfiguration_uses_one_coherent_settings_snapshot() {
use crate::pretrade::ConfigurablePolicy;
struct ReconfiguringTradeAmountOrder {
settings: LocalConfigCell<OrderSizeLimitSettings>,
instrument: Instrument,
account_id: AccountId,
}
impl HasInstrument for ReconfiguringTradeAmountOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
impl HasAccountId for ReconfiguringTradeAmountOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Ok(self.account_id)
}
}
impl HasTradeAmount for ReconfiguringTradeAmountOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
self.settings
.update::<OrderSizeLimitPolicyError>(|settings| {
settings.set_account_asset_barriers([OrderSizeAccountAssetBarrier {
limit: quantity_limit("10"),
account_id: self.account_id,
asset: Asset::new("AAPL").expect("asset code must be valid"),
}])
})
.expect("trade amount accessor must reconfigure policy");
Ok(TradeAmount::Quantity(
Quantity::from_str("11").expect("quantity literal must be valid"),
))
}
}
impl HasOrderPrice for ReconfiguringTradeAmountOrder {
fn price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(None)
}
}
let account_id = AccountId::from_u64(99224416);
let p = policy(Some(broker_quantity_barrier("100")), [], []);
let reconfiguring_order = ReconfiguringTradeAmountOrder {
settings: p.settings_cell(),
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id,
};
<TestPolicy as PreTradePolicy<
ReconfiguringTradeAmountOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p,
&PreTradeContext::<NoLocking>::new(None),
&reconfiguring_order,
)
.expect("the pre-trade-amount snapshot must apply the broker cap");
let reject = check(&p, &order_for_account("USD", "11", "1", account_id))
.expect_err("the reconfigured account+asset cap must apply to later orders");
assert_eq!(reject[0].code, RejectCode::OrderQtyExceedsLimit);
assert_eq!(reject[0].details, "requested 11, max allowed: 10");
}
#[test]
fn resolve_notional_covers_volume_and_missing_price_paths() {
let from_volume = super::resolve_notional(
OrderSizeLimitPolicy::<NoLocking>::NAME,
TradeAmount::Volume(Volume::from_str("123").expect("volume literal must be valid")),
None,
)
.expect("volume amount should resolve notional without price");
assert_eq!(
from_volume,
Volume::from_str("123").expect("volume literal must be valid")
);
let missing_price = super::resolve_notional(
OrderSizeLimitPolicy::<NoLocking>::NAME,
TradeAmount::Quantity(Quantity::from_str("1").expect("quantity literal must be valid")),
None,
)
.expect_err("quantity amount without price must reject");
assert_eq!(missing_price.code, RejectCode::OrderValueCalculationFailed);
assert_eq!(
missing_price.details,
"price not provided for evaluating cash flow/notional/volume"
);
}
#[test]
fn volume_order_without_price_propagates_resolve_quantity_error() {
let p = policy(None, [asset_quantity_barrier("AAPL", "100")], []);
let order_val = OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
trade_amount: TradeAmount::Volume(
Volume::from_str("100").expect("volume literal must be valid"),
),
price: None,
};
let reject = check(&p, &order_val).expect_err("volume order without price must reject");
let reject = &reject[0];
assert_eq!(reject.code, RejectCode::OrderValueCalculationFailed);
}
#[test]
fn resolve_quantity_covers_zero_price_and_missing_price_paths() {
let zero_quantity = super::resolve_quantity(
OrderSizeLimitPolicy::<NoLocking>::NAME,
TradeAmount::Volume(Volume::from_str("10").expect("volume literal must be valid")),
Some(Price::from_str("0").expect("zero price literal must be valid")),
)
.expect("volume-to-quantity conversion with zero price must pass");
assert_eq!(zero_quantity, Quantity::ZERO);
let missing_price = super::resolve_quantity(
OrderSizeLimitPolicy::<NoLocking>::NAME,
TradeAmount::Volume(Volume::from_str("10").expect("volume literal must be valid")),
None,
)
.expect_err("volume amount without price must reject");
assert_eq!(missing_price.code, RejectCode::OrderValueCalculationFailed);
assert_eq!(
missing_price.details,
"price not provided for evaluating cash flow/notional/volume"
);
}
#[test]
fn volume_overflow_is_treated_as_calculation_failed() {
let p = policy(None, [asset_notional_barrier("USD", "1000")], []);
let order_val = OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id: AccountId::from_u64(99224416),
side: crate::param::Side::Buy,
trade_amount: TradeAmount::Quantity(
Quantity::from_str("2").expect("quantity literal must be valid"),
),
price: Some(crate::param::Price::new(Decimal::MAX)),
};
let reject =
check(&p, &order_val).expect_err("overflow must be treated as calculation failed");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::OrderValueCalculationFailed);
assert_eq!(reject.reason, "order value calculation failed");
assert_eq!(
reject.details,
"price or quantity could not be used to evaluate order notional"
);
}
#[test]
fn broker_barrier_ignores_instrument_access_error() {
let p = policy(Some(broker_barrier("10", "1000")), [], []);
let order_val = InstrumentAccessErrorOrder;
assert!(<TestPolicy as PreTradePolicy<
InstrumentAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.is_ok());
}
#[test]
fn asset_barrier_maps_instrument_access_error_to_missing_required_field() {
let p = policy(None, [asset_barrier("USD", "10", "1000")], []);
let order_val = InstrumentAccessErrorOrder;
let reject = <TestPolicy as PreTradePolicy<
InstrumentAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.expect_err("field access error must reject");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::MissingRequiredField);
assert_eq!(
reject.reason,
"failed to access required field 'instrument'"
);
assert_eq!(reject.details, "failed to access field 'instrument'");
}
#[test]
fn instrument_access_error_precedes_account_id_access_error() {
let p = policy(
None,
[],
[OrderSizeAccountAssetBarrier {
limit: limit("10", "1000"),
account_id: AccountId::from_u64(1),
asset: Asset::new("AAPL").expect("asset code must be valid"),
}],
);
let order_val = InstrumentAccessErrorOrder;
let reject = <TestPolicy as PreTradePolicy<
InstrumentAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.expect_err("instrument access error must reject before account ID access");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::MissingRequiredField);
assert_eq!(
reject.reason,
"failed to access required field 'instrument'"
);
assert_eq!(reject.details, "failed to access field 'instrument'");
}
#[test]
fn broker_barrier_ignores_account_id_access_error() {
let p = policy(Some(broker_barrier("10", "1000")), [], []);
let order_val = AccountAccessErrorOrder {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
trade_amount_access_count: Cell::new(0),
trade_amount_access_error: false,
};
assert!(<TestPolicy as PreTradePolicy<
AccountAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.is_ok());
}
#[test]
fn unmatched_asset_barrier_does_not_access_trade_amount() {
let p = policy(None, [asset_quantity_barrier("IBM", "10")], []);
let order_val = AccountAccessErrorOrder {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
trade_amount_access_count: Cell::new(0),
trade_amount_access_error: true,
};
assert!(<TestPolicy as PreTradePolicy<
AccountAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.is_ok());
assert_eq!(
order_val.trade_amount_access_count.get(),
0,
"an unmatched asset barrier must not access the trade amount"
);
}
#[test]
fn instrument_and_account_id_are_accessed_only_for_their_axes() {
let account_id = AccountId::from_u64(99224416);
let order_val = AccountAccessCountingOrder {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id,
instrument_access_count: Rc::new(Cell::new(0)),
account_id_access_count: Rc::new(Cell::new(0)),
trade_amount_access_count: Rc::new(Cell::new(0)),
price_access_count: Rc::new(Cell::new(0)),
};
let check_order = |policy: &TestPolicy| {
<TestPolicy as PreTradePolicy<
AccountAccessCountingOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
policy, &PreTradeContext::<NoLocking>::new(None), &order_val
)
};
let broker_policy = policy(Some(broker_quantity_barrier("10")), [], []);
assert!(check_order(&broker_policy).is_ok());
assert_eq!(
order_val.instrument_access_count.get(),
0,
"broker-only settings must not access the instrument"
);
assert_eq!(
order_val.account_id_access_count.get(),
0,
"broker-only settings must not access the account ID"
);
let asset_policy = policy(None, [asset_quantity_barrier("AAPL", "10")], []);
assert!(check_order(&asset_policy).is_ok());
assert_eq!(
order_val.instrument_access_count.get(),
1,
"asset settings must access the instrument once"
);
assert_eq!(
order_val.account_id_access_count.get(),
0,
"asset-only settings must not access the account ID"
);
let account_asset_policy = policy(
None,
[],
[OrderSizeAccountAssetBarrier {
limit: quantity_limit("10"),
account_id,
asset: Asset::new("AAPL").expect("asset code must be valid"),
}],
);
assert!(check_order(&account_asset_policy).is_ok());
assert_eq!(
order_val.instrument_access_count.get(),
2,
"account+asset settings must access the instrument once"
);
assert_eq!(
order_val.account_id_access_count.get(),
1,
"account+asset settings must access the account ID once"
);
}
#[test]
fn drop_copy_only_reads_the_engine_required_account_id() {
let account_id = AccountId::from_u64(99224416);
let p = policy(
Some(broker_barrier("10", "1000")),
[asset_barrier("AAPL", "10", "1000")],
[OrderSizeAccountAssetBarrier {
limit: limit("10", "1000"),
account_id,
asset: Asset::new("AAPL").expect("asset code must be valid"),
}],
);
let order_val = AccountAccessCountingOrder {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id,
instrument_access_count: Rc::new(Cell::new(0)),
account_id_access_count: Rc::new(Cell::new(0)),
trade_amount_access_count: Rc::new(Cell::new(0)),
price_access_count: Rc::new(Cell::new(0)),
};
let engine = Engine::builder::<AccountAccessCountingOrder, (), ()>()
.no_sync()
.pre_trade(p)
.build()
.expect("engine must build");
let mut operation = engine
.apply_drop_copy(order_val.clone())
.expect("drop-copy order must bypass size limits");
operation.commit();
assert_eq!(
order_val.instrument_access_count.get(),
0,
"drop-copy must not access the instrument"
);
assert_eq!(
order_val.account_id_access_count.get(),
1,
"only the engine must access the account ID"
);
assert_eq!(
order_val.trade_amount_access_count.get(),
0,
"drop-copy must not access the trade amount"
);
assert_eq!(
order_val.price_access_count.get(),
0,
"drop-copy must not access the price"
);
}
#[test]
fn account_id_access_error_precedes_trade_amount_access_error() {
let p = policy(
None,
[],
[OrderSizeAccountAssetBarrier {
limit: limit("10", "1000"),
account_id: AccountId::from_u64(1),
asset: Asset::new("AAPL").expect("asset code must be valid"),
}],
);
let order_val = AccountAccessErrorOrder {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
trade_amount_access_count: Cell::new(0),
trade_amount_access_error: true,
};
let reject = <TestPolicy as PreTradePolicy<
AccountAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.expect_err("field access error must reject");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::MissingRequiredField);
assert_eq!(
reject.reason,
"failed to access required field 'account ID'"
);
assert_eq!(reject.details, "failed to access field 'account_id'");
assert_eq!(
order_val.trade_amount_access_count.get(),
0,
"an account ID access error must precede trade amount access"
);
}
#[test]
fn maps_trade_amount_access_error_to_missing_required_field() {
struct TradeAmountAccessErrorOrder {
instrument: Instrument,
}
impl HasInstrument for TradeAmountAccessErrorOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
impl HasAccountId for TradeAmountAccessErrorOrder {
fn account_id(&self) -> Result<AccountId, crate::RequestFieldAccessError> {
Ok(AccountId::from_u64(1))
}
}
impl HasTradeAmount for TradeAmountAccessErrorOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("trade_amount"))
}
}
impl HasOrderPrice for TradeAmountAccessErrorOrder {
fn price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(Some(
Price::from_str("1").expect("price literal must be valid"),
))
}
}
let p = policy(None, [asset_barrier("USD", "10", "1000")], []);
let order_val = TradeAmountAccessErrorOrder {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
};
let reject = <TestPolicy as PreTradePolicy<
TradeAmountAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.expect_err("field access error must reject");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::MissingRequiredField);
assert_eq!(
reject.reason,
"failed to access required field 'trade amount'"
);
assert_eq!(reject.details, "failed to access field 'trade_amount'");
}
#[test]
fn maps_price_access_error_to_missing_required_field() {
struct PriceAccessErrorOrder {
instrument: Instrument,
}
impl HasInstrument for PriceAccessErrorOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
impl HasAccountId for PriceAccessErrorOrder {
fn account_id(&self) -> Result<AccountId, crate::RequestFieldAccessError> {
Ok(AccountId::from_u64(1))
}
}
impl HasTradeAmount for PriceAccessErrorOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
Ok(TradeAmount::Quantity(
Quantity::from_str("1").expect("quantity literal must be valid"),
))
}
}
impl HasOrderPrice for PriceAccessErrorOrder {
fn price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("price"))
}
}
let p = policy(None, [asset_notional_barrier("USD", "1000")], []);
let order_val = PriceAccessErrorOrder {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
};
let reject = <TestPolicy as PreTradePolicy<
PriceAccessErrorOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&p, &PreTradeContext::<NoLocking>::new(None), &order_val
)
.expect_err("field access error must reject");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::MissingRequiredField);
assert_eq!(reject.reason, "failed to access required field 'price'");
assert_eq!(reject.details, "failed to access field 'price'");
}
}