use std::cell::RefCell;
use std::collections::HashMap;
use crate::param::{Asset, Price, Quantity, TradeAmount, Volume};
use crate::pretrade::{CheckPreTradeStartPolicy, Reject, RejectCode, RejectScope};
use crate::HasInstrument;
use crate::{HasOrderPrice, HasTradeAmount};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderSizeLimit {
pub max_notional: Volume,
pub max_quantity: Quantity,
pub settlement_asset: Asset,
}
pub struct OrderSizeLimitPolicy {
limits: RefCell<HashMap<Asset, OrderSizeLimit>>,
}
impl OrderSizeLimitPolicy {
pub const NAME: &'static str = "OrderSizeLimitPolicy";
pub fn new(
initial_limit: OrderSizeLimit,
additional_limits: impl IntoIterator<Item = OrderSizeLimit>,
) -> Self {
let mut limits = HashMap::new();
limits.insert(initial_limit.settlement_asset.clone(), initial_limit);
for limit in additional_limits {
limits.insert(limit.settlement_asset.clone(), limit);
}
Self {
limits: RefCell::new(limits),
}
}
pub fn set_limit(&self, limit: OrderSizeLimit) {
self.limits
.borrow_mut()
.insert(limit.settlement_asset.clone(), limit);
}
}
impl<O, R> CheckPreTradeStartPolicy<O, R> for OrderSizeLimitPolicy
where
O: HasInstrument + HasTradeAmount + HasOrderPrice,
{
fn name(&self) -> &'static str {
Self::NAME
}
fn check_pre_trade_start(&self, order: &O) -> Result<(), Reject> {
let limits = self.limits.borrow();
check_pre_trade_start_with_limits(
Self::NAME,
&limits,
order.instrument().settlement_asset(),
order.trade_amount(),
order.price(),
)
}
fn apply_execution_report(&self, _report: &R) -> bool {
false
}
}
fn check_pre_trade_start_with_limits(
policy: &'static str,
limits: &HashMap<Asset, OrderSizeLimit>,
settlement: &Asset,
trade_amount: TradeAmount,
price: Option<Price>,
) -> Result<(), Reject> {
let limit = match limits.get(settlement).cloned() {
Some(value) => value,
None => return Err(missing_order_size_limit_reject(policy, settlement)),
};
let quantity = resolve_quantity(policy, trade_amount, price)?;
let requested_notional = resolve_notional(policy, trade_amount, price)?;
let quantity_exceeded = quantity > limit.max_quantity;
let notional_exceeded = requested_notional > limit.max_notional;
match (quantity_exceeded, notional_exceeded) {
(false, false) => Ok(()),
(true, false) => Err(Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderQtyExceedsLimit,
"order quantity exceeded",
format!("requested {quantity}, max allowed: {}", limit.max_quantity),
)),
(false, true) => Err(order_notional_reject(policy, &limit, requested_notional)),
(true, true) => Err(order_size_reject(
policy,
quantity,
&limit,
requested_notional,
)),
}
}
fn resolve_notional(
policy: &'static 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: &'static 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 missing_order_size_limit_reject(policy: &'static str, settlement: &Asset) -> Reject {
Reject::new(
policy,
RejectScope::Order,
RejectCode::RiskConfigurationMissing,
"order size limit missing",
format!("settlement asset {settlement} has no configured limit"),
)
}
fn order_value_calculation_failed_reject(policy: &'static str, details: &'static str) -> Reject {
Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderValueCalculationFailed,
"order value calculation failed",
details,
)
}
fn order_notional_reject(
policy: &'static str,
limit: &OrderSizeLimit,
requested_notional: Volume,
) -> Reject {
let details = format!(
"requested {requested_notional}, max allowed: {}",
limit.max_notional
);
Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderNotionalExceedsLimit,
"order notional exceeded",
details,
)
}
fn order_size_reject(
policy: &'static str,
quantity: crate::param::Quantity,
limit: &OrderSizeLimit,
requested_notional: Volume,
) -> Reject {
Reject::new(
policy,
RejectScope::Order,
RejectCode::OrderExceedsLimit,
"order size exceeded",
format!(
"requested quantity {quantity}, max allowed: {}; requested notional {requested_notional}, max allowed: {}",
limit.max_quantity,
limit.max_notional
),
)
}
#[cfg(test)]
mod tests {
use crate::core::{Instrument, OrderOperation};
use crate::param::TradeAmount;
use crate::param::{Asset, Price, Quantity, Side, Volume};
use crate::pretrade::{CheckPreTradeStartPolicy, RejectCode, RejectScope};
use rust_decimal::Decimal;
use super::{OrderSizeLimit, OrderSizeLimitPolicy};
type TestOrder = OrderOperation;
fn order(settlement: &str, quantity: &str, price: &str) -> TestOrder {
OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new(settlement).expect("asset code must be valid"),
),
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")),
}
}
#[test]
fn quantity_violation_returns_order_quantity_exceeded() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "10", "1000"), no_limits());
let reject =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order("USD", "11", "90"),
)
.expect_err("quantity must be rejected");
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");
}
#[test]
fn notional_violation_returns_order_notional_exceeded() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "10", "1000"), no_limits());
let reject =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order("USD", "10", "101"),
)
.expect_err("notional must be rejected");
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");
}
#[test]
fn both_violations_are_returned_in_single_reject() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "10", "1000"), no_limits());
let reject =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order("USD", "11", "100"),
)
.expect_err("quantity and notional must be rejected");
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, max allowed: 10; requested notional 1100, max allowed: 1000"
);
}
#[test]
fn missing_limit_returns_order_size_limit_missing() {
let policy = OrderSizeLimitPolicy::new(limit("EUR", "10", "1000"), no_limits());
let reject =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order("USD", "1", "1"),
)
.expect_err("missing limit must reject");
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::RiskConfigurationMissing);
assert_eq!(reject.reason, "order size limit missing");
assert_eq!(
reject.details,
"settlement asset USD has no configured limit"
);
}
#[test]
fn boundary_values_are_accepted() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "10", "1000"), no_limits());
let result =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order("USD", "10", "100"),
);
assert!(result.is_ok());
}
#[test]
fn unconfigured_settlement_rejects_when_limit_is_missing() {
let policy = OrderSizeLimitPolicy::new(limit("EUR", "10", "1000"), no_limits());
let reject =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order("USD", "1", "1"),
)
.expect_err("default policy must reject without configured limits");
assert_eq!(reject.code, RejectCode::RiskConfigurationMissing);
assert_eq!(reject.reason, "order size limit missing");
assert_eq!(
reject.details,
"settlement asset USD has no configured limit"
);
}
#[test]
fn volume_overflow_is_treated_as_notional_exceeded() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "100", "1000"), no_limits());
let order = OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
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 =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order,
)
.expect_err("overflow must be treated as notional exceeded");
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 volume_overflow_with_quantity_violation_returns_order_size_exceeded() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "1", "1000"), no_limits());
let order = OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
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 =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy,
&order,
)
.expect_err("overflow plus quantity violation must be order size exceeded");
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 additional_limits_and_set_limit_are_applied() {
let policy = OrderSizeLimitPolicy::new(
limit("USD", "10", "1000"),
vec![limit("EUR", "5", "500"), limit("GBP", "3", "300")],
);
assert!(<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(&policy, &order("EUR", "5", "100")).is_ok());
assert!(<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(&policy, &order("GBP", "3", "100")).is_ok());
policy.set_limit(limit("EUR", "1", "100"));
let reject = <OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(&policy, &order("EUR", "2", "10"))
.expect_err("updated limit must be enforced");
assert_eq!(reject.code, RejectCode::OrderQtyExceedsLimit);
assert_eq!(reject.details, "requested 2, max allowed: 1");
}
#[test]
fn policy_name_is_stable() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "10", "1000"), no_limits());
assert_eq!(
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::name(&policy),
OrderSizeLimitPolicy::NAME
);
}
#[test]
fn apply_execution_report_returns_false() {
let policy = OrderSizeLimitPolicy::new(limit("USD", "10", "1000"), no_limits());
assert!(!<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<
TestOrder,
(),
>>::apply_execution_report(&policy, &()));
}
#[test]
fn resolve_notional_covers_volume_and_missing_price_paths() {
let from_volume = super::resolve_notional(
OrderSizeLimitPolicy::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::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 policy = OrderSizeLimitPolicy::new(limit("USD", "100", "10000"), no_limits());
let order = OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
side: Side::Buy,
trade_amount: TradeAmount::Volume(
Volume::from_str("100").expect("volume literal must be valid"),
),
price: None,
};
let reject =
<OrderSizeLimitPolicy as CheckPreTradeStartPolicy<TestOrder, ()>>::check_pre_trade_start(
&policy, &order,
)
.expect_err("volume order without price must reject");
assert_eq!(reject.code, RejectCode::OrderValueCalculationFailed);
}
#[test]
fn resolve_quantity_covers_invalid_volume_conversion_and_missing_price_paths() {
let conversion_failed = super::resolve_quantity(
OrderSizeLimitPolicy::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_err("volume-to-quantity conversion with zero price must reject");
assert_eq!(
conversion_failed.code,
RejectCode::OrderValueCalculationFailed
);
assert_eq!(
conversion_failed.details,
"price or volume could not be used to evaluate order quantity"
);
let missing_price = super::resolve_quantity(
OrderSizeLimitPolicy::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"
);
}
fn limit(settlement: &str, max_quantity: &str, max_notional: &str) -> OrderSizeLimit {
OrderSizeLimit {
max_notional: Volume::from_str(max_notional)
.expect("max notional literal must be valid"),
max_quantity: Quantity::from_str(max_quantity)
.expect("max quantity literal must be valid"),
settlement_asset: Asset::new(settlement).expect("asset code must be valid"),
}
}
fn no_limits() -> Vec<OrderSizeLimit> {
Vec::new()
}
}