use crate::core::account_outcome::AccountOutcomeEntry;
pub use crate::core::{PolicyGroupId, DEFAULT_POLICY_GROUP_ID};
use super::{
AccountBlock, PolicyPreTradeResult, PostTradeContext, PostTradeResult, PreTradeContext, Reject,
RejectCode, RejectScope, Rejects,
};
use crate::core::SyncMode;
use crate::param::AccountId;
use crate::{AccountAdjustmentContext, Mutations};
pub trait PreTradePolicy<Order, ExecutionReport, AccountAdjustment, Sync>
where
Sync: SyncMode + ?Sized,
{
fn name(&self) -> &str;
fn policy_group_id(&self) -> PolicyGroupId {
DEFAULT_POLICY_GROUP_ID
}
#[doc(hidden)]
#[allow(private_interfaces)]
fn built_in_config_entry(
&self,
) -> Option<crate::core::ConfigEntry<<Sync as SyncMode>::StorageLockingPolicyFactory>> {
None
}
fn check_pre_trade_start(
&self,
_ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
_order: &Order,
) -> Result<(), Rejects> {
Ok(())
}
fn check_pre_trade_start_dry_run(
&self,
ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
order: &Order,
) -> Result<(), Rejects> {
self.check_pre_trade_start(ctx, order)
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
_order: &Order,
_mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
Ok(None)
}
fn perform_pre_trade_check_dry_run(
&self,
ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
order: &Order,
mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
self.perform_pre_trade_check(ctx, order, mutations)
}
fn apply_execution_report(
&self,
_ctx: &PostTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
_report: &ExecutionReport,
) -> Option<PostTradeResult> {
None
}
fn apply_account_adjustment(
&self,
_ctx: &AccountAdjustmentContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
_account_id: AccountId,
_adjustment: &AccountAdjustment,
_mutations: &mut Mutations,
) -> Result<Vec<AccountOutcomeEntry>, Rejects> {
Ok(vec![])
}
}
pub(crate) trait PolicyName {
fn policy_name(&self) -> &str;
}
pub(crate) fn missing_required_field_reject<Policy: PolicyName + ?Sized>(
policy: &Policy,
field_name: &str,
error: &crate::RequestFieldAccessError,
) -> Reject {
Reject::new(
policy.policy_name(),
RejectScope::Order,
RejectCode::MissingRequiredField,
format!("failed to access required field '{field_name}'"),
error.to_string(),
)
}
pub(crate) fn missing_required_field_account_adjustment_reject<Policy: PolicyName + ?Sized>(
policy: &Policy,
field_name: &str,
error: &crate::RequestFieldAccessError,
) -> Reject {
Reject::new(
policy.policy_name(),
RejectScope::Account,
RejectCode::MissingRequiredField,
format!("failed to access required field '{field_name}'"),
error.to_string(),
)
}
pub(crate) fn field_access_error_account_adjustment_reject<Policy: PolicyName + ?Sized>(
policy: &Policy,
field_name: &str,
error: &crate::RequestFieldAccessError,
) -> Reject {
Reject::new(
policy.policy_name(),
RejectScope::Account,
RejectCode::InvalidFieldFormat,
format!("failed to access field '{field_name}'"),
error.to_string(),
)
}
pub(crate) fn missing_required_field_account_block<Policy: PolicyName + ?Sized>(
policy: &Policy,
field_name: &str,
error: &crate::RequestFieldAccessError,
) -> AccountBlock {
AccountBlock::new(
policy.policy_name(),
RejectCode::MissingRequiredField,
format!("failed to access required field '{field_name}'"),
error.to_string(),
)
}
#[cfg(test)]
mod tests {
use crate::core::{
ExecutionReportOperation, FinancialImpact, OrderOperation, WithExecutionReportOperation,
WithFinancialImpact,
};
use crate::param::{AccountId, Asset, Fee, Pnl, Quantity, Side, TradeAmount};
use crate::pretrade::{PolicyPreTradeResult, RejectCode, RejectScope, Rejects};
use crate::{AccountOutcomeEntry, Mutations, RequestFieldAccessError};
use crate::core::{LocalSync, SyncMode};
use crate::storage::NoLocking;
use super::{
missing_required_field_reject, PolicyName, PostTradeContext, PreTradeContext,
PreTradePolicy,
};
type TestOrder = OrderOperation;
type TestReport = WithExecutionReportOperation<WithFinancialImpact<()>>;
struct MainPolicyNoop;
struct StartPolicyNoop;
struct AccountAdjustmentHookNoop;
impl<Sync: SyncMode> PreTradePolicy<TestOrder, TestReport, (), Sync> for StartPolicyNoop {
fn name(&self) -> &str {
"StartPolicyNoop"
}
fn check_pre_trade_start(
&self,
_ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
_order: &TestOrder,
) -> Result<(), Rejects> {
Ok(())
}
}
impl<Sync: SyncMode> PreTradePolicy<TestOrder, TestReport, (), Sync> for MainPolicyNoop {
fn name(&self) -> &str {
"MainPolicyNoop"
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
_order: &TestOrder,
_mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
Ok(None)
}
}
impl<Sync: SyncMode> PreTradePolicy<TestOrder, TestReport, (), Sync> for AccountAdjustmentHookNoop {
fn name(&self) -> &str {
"AccountAdjustmentHookNoop"
}
fn apply_account_adjustment(
&self,
_ctx: &crate::AccountAdjustmentContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
_account_id: AccountId,
_adjustment: &(),
_mutations: &mut Mutations,
) -> Result<Vec<AccountOutcomeEntry>, Rejects> {
Ok(vec![])
}
}
#[test]
fn apply_execution_report_hook_returns_empty_for_noop_main_policy() {
let report = WithExecutionReportOperation {
inner: WithFinancialImpact {
inner: (),
financial_impact: FinancialImpact {
pnl: Pnl::from_str("0").expect("pnl must be valid"),
fee: Fee::ZERO,
},
},
operation: ExecutionReportOperation {
instrument: crate::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,
},
};
assert!(
<MainPolicyNoop as PreTradePolicy<TestOrder, TestReport, (), LocalSync>>::apply_execution_report(
&MainPolicyNoop,
&PostTradeContext::new(),
&report,
)
.is_none()
);
}
#[test]
fn apply_execution_report_hook_returns_empty_for_noop_start_policy() {
let report = WithExecutionReportOperation {
inner: WithFinancialImpact {
inner: (),
financial_impact: FinancialImpact {
pnl: Pnl::from_str("0").expect("pnl must be valid"),
fee: Fee::ZERO,
},
},
operation: ExecutionReportOperation {
instrument: crate::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,
},
};
assert!(
<StartPolicyNoop as PreTradePolicy<TestOrder, TestReport, (), LocalSync>>::apply_execution_report(
&StartPolicyNoop,
&PostTradeContext::new(),
&report,
)
.is_none()
);
}
#[test]
fn required_trait_methods_can_be_invoked_without_side_effects() {
let order = OrderOperation {
instrument: crate::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: None,
};
let mut mutations = Mutations::new();
assert_eq!(
<MainPolicyNoop as PreTradePolicy<TestOrder, TestReport, (), LocalSync>>::name(
&MainPolicyNoop
),
"MainPolicyNoop"
);
let result = <MainPolicyNoop as PreTradePolicy<TestOrder, TestReport, (), LocalSync>>::perform_pre_trade_check(
&MainPolicyNoop,
&PreTradeContext::<NoLocking>::new(None),
&order,
&mut mutations,
);
assert!(mutations.is_empty());
assert!(result.is_ok());
}
#[test]
fn required_start_trait_methods_can_be_invoked_without_side_effects() {
let order = OrderOperation {
instrument: crate::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: None,
};
assert_eq!(
<StartPolicyNoop as PreTradePolicy<TestOrder, TestReport, (), LocalSync>>::name(
&StartPolicyNoop
),
"StartPolicyNoop"
);
assert!(
<StartPolicyNoop as PreTradePolicy<TestOrder, TestReport, (), LocalSync>>::check_pre_trade_start(
&StartPolicyNoop,
&PreTradeContext::<NoLocking>::new(None),
&order,
)
.is_ok()
);
}
#[test]
fn required_account_adjustment_trait_methods_can_be_invoked_without_side_effects() {
let mut mutations = Mutations::new();
assert_eq!(
<AccountAdjustmentHookNoop as PreTradePolicy<TestOrder, TestReport, (), LocalSync>>::name(
&AccountAdjustmentHookNoop
),
"AccountAdjustmentHookNoop"
);
use crate::core::account_control::BlockedAccounts;
use crate::core::{AccountBlockHandle, AccountControl};
use crate::storage::{LockingPolicyFactory, NoLocking, StorageBuilder};
let builder = StorageBuilder::new(NoLocking);
let handle =
AccountBlockHandle::from_inner(NoLocking::new_shared(BlockedAccounts::new(&builder)));
let ctrl = AccountControl::new(handle, AccountId::from_u64(99224416));
let result = <AccountAdjustmentHookNoop as PreTradePolicy<
TestOrder,
TestReport,
(),
LocalSync,
>>::apply_account_adjustment(
&AccountAdjustmentHookNoop,
&crate::AccountAdjustmentContext::new_test(ctrl),
AccountId::from_u64(99224416),
&(),
&mut mutations,
);
assert!(mutations.is_empty());
assert!(result.is_ok());
}
#[test]
fn missing_required_field_reject_builds_payload_from_policy_field_and_error() {
struct TestPolicyTag;
impl PolicyName for TestPolicyTag {
fn policy_name(&self) -> &str {
"TestPolicy"
}
}
let error = RequestFieldAccessError::new("instrument");
let reject = missing_required_field_reject(&TestPolicyTag, "instrument", &error);
assert_eq!(reject.policy, "TestPolicy");
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'");
}
}