use crate::core::HasTradeAmount;
use crate::param::TradeAmount;
use crate::pretrade::policy::{missing_required_field_reject, PolicyGroupId, PolicyName};
use crate::pretrade::DEFAULT_POLICY_GROUP_ID;
use crate::pretrade::{PreTradeContext, PreTradePolicy, Reject, RejectCode, RejectScope, Rejects};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct OrderValidationPolicy {
group_id: PolicyGroupId,
}
impl OrderValidationPolicy {
pub const NAME: &'static str = "OrderValidationPolicy";
pub fn new() -> Self {
Self {
group_id: DEFAULT_POLICY_GROUP_ID,
}
}
pub fn with_policy_group_id(mut self, id: PolicyGroupId) -> Self {
self.group_id = id;
self
}
}
impl PolicyName for OrderValidationPolicy {
fn policy_name(&self) -> &str {
Self::NAME
}
}
impl<Order, ExecutionReport, AccountAdjustment, Sync>
PreTradePolicy<Order, ExecutionReport, AccountAdjustment, Sync> for OrderValidationPolicy
where
Order: HasTradeAmount,
Sync: crate::core::SyncMode,
{
fn name(&self) -> &str {
Self::NAME
}
fn policy_group_id(&self) -> PolicyGroupId {
self.group_id
}
fn check_pre_trade_start(
&self,
_ctx: &PreTradeContext<<Sync as crate::core::SyncMode>::StorageLockingPolicyFactory>,
order: &Order,
) -> Result<(), Rejects> {
match order
.trade_amount()
.map_err(|e| Rejects::from(missing_required_field_reject(self, "trade amount", &e)))?
{
TradeAmount::Quantity(quantity) if quantity.is_zero() => {
return Err(Reject::new(
Self::NAME,
RejectScope::Order,
RejectCode::InvalidFieldValue,
"order quantity must be non-zero",
"requested quantity 0 is not allowed",
)
.into());
}
TradeAmount::Volume(volume) if volume.is_zero() => {
return Err(Reject::new(
Self::NAME,
RejectScope::Order,
RejectCode::InvalidFieldValue,
"order volume must be non-zero",
"requested volume 0 is not allowed",
)
.into());
}
_ => {}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::core::{Instrument, OrderOperation};
use crate::param::{AccountId, Asset, Price, Quantity, Side, TradeAmount, Volume};
use crate::pretrade::{PreTradeContext, PreTradePolicy, RejectCode, RejectScope};
use crate::storage::NoLocking;
use crate::RequestFieldAccessError;
use super::OrderValidationPolicy;
#[test]
fn rejects_zero_quantity() {
let policy = OrderValidationPolicy::new();
let 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::ZERO),
price: Some(Price::from_str("10").expect("price must be valid")),
};
let reject = <OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&policy, &PreTradeContext::<NoLocking>::new(None), &order
)
.expect_err("zero quantity must be rejected");
let reject = &reject[0];
assert_eq!(reject.scope, RejectScope::Order);
assert_eq!(reject.code, RejectCode::InvalidFieldValue);
assert_eq!(reject.reason, "order quantity must be non-zero");
assert_eq!(reject.details, "requested quantity 0 is not allowed");
}
#[test]
fn rejects_zero_volume() {
let policy = OrderValidationPolicy::new();
let 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::ZERO),
price: None,
};
let reject = <OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&policy, &PreTradeContext::<NoLocking>::new(None), &order
)
.expect_err("zero volume must be rejected");
let reject = &reject[0];
assert_eq!(reject.code, RejectCode::InvalidFieldValue);
assert_eq!(reject.reason, "order volume must be non-zero");
}
#[test]
fn allows_missing_price_when_volume_is_present() {
let policy = OrderValidationPolicy::new();
let 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 must be valid"),
),
price: None,
};
assert!(<OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&policy, &PreTradeContext::<NoLocking>::new(None), &order
)
.is_ok());
}
#[test]
fn allows_zero_and_negative_price() {
let policy = OrderValidationPolicy::new();
let zero_price_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 must be valid"),
),
price: Some(Price::ZERO),
};
assert!(<OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&policy,
&PreTradeContext::<NoLocking>::new(None),
&zero_price_order
)
.is_ok());
let negative_price_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 must be valid"),
),
price: Some(Price::from_str("-5").expect("price must be valid")),
};
assert!(<OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&policy,
&PreTradeContext::<NoLocking>::new(None),
&negative_price_order
)
.is_ok());
}
#[test]
fn policy_name_is_stable() {
let policy = OrderValidationPolicy::new();
assert_eq!(
<OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::name(&policy),
OrderValidationPolicy::NAME
);
}
#[test]
fn apply_execution_report_returns_false() {
let policy = OrderValidationPolicy::new();
let 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("1").expect("quantity must be valid"),
),
price: Some(Price::from_str("10").expect("price must be valid")),
};
assert!(<OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::apply_execution_report(
&policy, &crate::pretrade::PostTradeContext::new(), &()
)
.is_none());
assert!(<OrderValidationPolicy as PreTradePolicy<
OrderOperation,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&policy, &PreTradeContext::<NoLocking>::new(None), &order
)
.is_ok());
}
#[test]
fn maps_trade_amount_access_error_to_missing_required_field() {
struct InvalidOrder;
impl crate::HasTradeAmount for InvalidOrder {
fn trade_amount(&self) -> Result<TradeAmount, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("trade_amount"))
}
}
let policy = OrderValidationPolicy::new();
let reject = <OrderValidationPolicy as PreTradePolicy<
InvalidOrder,
(),
(),
crate::core::LocalSync,
>>::check_pre_trade_start(
&policy,
&PreTradeContext::<NoLocking>::new(None),
&InvalidOrder,
)
.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'");
}
}