use std::sync::Arc;
use std::time::Duration;
use openpit::marketdata::{InstrumentId, MarketDataBuilder, Quote, QuoteTtl};
use openpit::param::{
AccountId, AdjustmentAmount, Asset, Fee, Pnl, PositionSize, Price, Quantity, Side, TradeAmount,
Volume,
};
use openpit::pretrade::policies::pnl_bounds_killswitch::PnlBoundsAccountAssetBarrierUpdate;
use openpit::pretrade::policies::{
OrderSizeAccountAssetBarrier, OrderSizeAssetBarrier, OrderSizeBrokerBarrier, OrderSizeLimit,
OrderSizeLimitPolicy, OrderSizeLimitPolicyError, OrderSizeLimitSettings,
PnlBoundsAccountAssetBarrier, PnlBoundsBrokerBarrier, PnlBoundsKillSwitchPolicy,
PnlBoundsKillSwitchPolicyError, PnlBoundsKillSwitchSettings, RateLimit, RateLimitAssetBarrier,
RateLimitBrokerBarrier, RateLimitPolicy, RateLimitPolicyError, RateLimitSettings,
SpotFundsOverride, SpotFundsOverrideTarget, SpotFundsPolicy, SpotFundsPricingSource,
SpotFundsSettings,
};
use openpit::pretrade::{PreTradePolicy, DEFAULT_POLICY_GROUP_ID};
use openpit::storage::{FullLocking, IndexLocking, NoLocking};
use openpit::{
AccountKeyConstraint, AccountSync, AccountSyncEngine, Configurator, ConfigureError, Engine,
ExecutionReportOperation, FinancialImpact, FullSync, FullSyncEngine,
HasAccountAdjustmentBalance, HasAccountAdjustmentBalanceAverageEntryPrice,
HasAccountAdjustmentBalanceLowerBound, HasAccountAdjustmentBalanceRealizedPnl,
HasAccountAdjustmentBalanceUpperBound, HasAccountAdjustmentHeld,
HasAccountAdjustmentHeldLowerBound, HasAccountAdjustmentHeldUpperBound,
HasAccountAdjustmentIncoming, HasAccountAdjustmentIncomingLowerBound,
HasAccountAdjustmentIncomingUpperBound, HasBalanceAsset, Instrument, LocalEngine, LocalSync,
OrderOperation, RequestFieldAccessError, SpotFundsConfigError, SpotFundsMarketData,
WithExecutionReportFillDetails, WithExecutionReportOperation, WithFinancialImpact,
};
type AccountLocking = IndexLocking<AccountKeyConstraint>;
struct CustomPolicy;
impl PreTradePolicy<OrderOperation, (), (), LocalSync> for CustomPolicy {
fn name(&self) -> &str {
"CustomPolicy"
}
}
fn order(account: u64) -> OrderOperation {
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(account),
side: Side::Buy,
trade_amount: TradeAmount::Quantity(
Quantity::from_str("1").expect("quantity literal must be valid"),
),
price: None,
}
}
fn broker_settings(max_orders: usize) -> RateLimitSettings {
RateLimitSettings::new(
Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders,
window: Duration::from_secs(60),
},
}),
[],
[],
[],
)
.expect("broker barrier is a valid configuration")
}
fn build_engine(max_orders: usize) -> FullSyncEngine<OrderOperation> {
let builder = Engine::builder::<OrderOperation, (), ()>().full_sync();
let policy =
RateLimitPolicy::<FullLocking>::new(broker_settings(max_orders), builder.storage_builder());
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
fn build_local_engine(max_orders: usize) -> LocalEngine<OrderOperation> {
let builder = Engine::builder::<OrderOperation, (), ()>().no_sync();
let policy =
RateLimitPolicy::<NoLocking>::new(broker_settings(max_orders), builder.storage_builder());
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
fn build_account_engine(max_orders: usize) -> AccountSyncEngine<OrderOperation> {
let builder = Engine::builder::<OrderOperation, (), ()>().account_sync();
let policy = RateLimitPolicy::<AccountLocking>::new(
broker_settings(max_orders),
builder.storage_builder(),
);
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
#[test]
fn configure_retunes_live_policy_behavior() {
let engine = build_engine(5);
let name = RateLimitPolicy::<FullLocking>::NAME;
for account in 0..3 {
engine
.execute_pre_trade(order(account))
.expect("order within the generous limit must pass");
}
engine
.configure()
.rate_limit::<RateLimitPolicyError>(name, |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 2,
window: Duration::from_secs(60),
},
}))
})
.expect("retune must publish");
let rejects = match engine.execute_pre_trade(order(3)) {
Ok(_) => panic!("order beyond the tightened limit must be rejected"),
Err(rejects) => rejects,
};
assert_eq!(rejects[0].reason, "rate limit exceeded: broker barrier");
}
#[test]
fn configure_rate_limit_retune_does_not_reset_surviving_broker_counter() {
let engine = build_engine(5);
let name = RateLimitPolicy::<FullLocking>::NAME;
engine
.execute_pre_trade(order(0))
.expect("first order under initial limit must pass");
engine
.execute_pre_trade(order(1))
.expect("second order under initial limit must pass");
engine
.configure()
.rate_limit::<RateLimitPolicyError>(name, |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 2,
window: Duration::from_secs(60),
},
}))
})
.expect("retune must publish without replacing the counter");
let rejects = engine
.execute_pre_trade(order(2))
.err()
.expect("third order in the surviving window must be rejected");
assert_eq!(rejects[0].reason, "rate limit exceeded: broker barrier");
assert!(rejects[0].details.contains("submitted 3 orders"));
}
#[test]
fn configure_adds_asset_barrier_at_runtime() {
let engine = build_engine(100);
let name = RateLimitPolicy::<FullLocking>::NAME;
engine
.configure()
.rate_limit::<RateLimitPolicyError>(name, |settings| {
settings.set_asset_barriers([RateLimitAssetBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
limit: RateLimit {
max_orders: 1,
window: Duration::from_secs(60),
},
}])
})
.expect("adding an asset barrier must publish");
engine
.execute_pre_trade(order(0))
.expect("first USD order under the new barrier must pass");
let rejects = match engine.execute_pre_trade(order(1)) {
Ok(_) => panic!("second USD order must breach the new asset barrier"),
Err(rejects) => rejects,
};
assert_eq!(rejects[0].reason, "rate limit exceeded: asset barrier");
}
#[test]
fn configure_unknown_policy_name_is_reported() {
let engine = build_engine(5);
let error = engine
.configure()
.rate_limit::<RateLimitPolicyError>("does-not-exist", |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1,
window: Duration::from_secs(60),
},
}))
})
.expect_err("an unknown policy name must be rejected");
assert_eq!(
error,
ConfigureError::UnknownPolicy {
name: "does-not-exist".to_owned(),
}
);
}
#[test]
fn configure_type_mismatch_is_reported() {
let engine = build_engine(5);
let name = RateLimitPolicy::<FullLocking>::NAME;
let error = engine
.configure()
.spot_funds::<std::convert::Infallible>(name, |_settings| Ok(()))
.expect_err("a typed method against the wrong settings type must fail");
assert!(
matches!(&error, ConfigureError::PolicyTypeMismatch { name: n, .. } if n.as_str() == name),
"expected PolicyTypeMismatch, got {error:?}"
);
}
#[test]
fn configure_validation_failure_keeps_prior_value() {
let engine = build_engine(2);
let name = RateLimitPolicy::<FullLocking>::NAME;
let error = engine
.configure()
.rate_limit::<RateLimitPolicyError>(name, |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1_000,
window: Duration::ZERO,
},
}))
})
.expect_err("an invalid window must be rejected");
assert!(
matches!(&error, ConfigureError::Validation { name: n, .. } if n.as_str() == name),
"expected Validation, got {error:?}"
);
engine
.execute_pre_trade(order(0))
.expect("first order under the retained limit must pass");
engine
.execute_pre_trade(order(0))
.expect("second order under the retained limit must pass");
assert!(
engine.execute_pre_trade(order(0)).is_err(),
"third order must still breach the retained limit of 2"
);
}
#[test]
fn configurator_send_sync_per_mode() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Configurator<FullSync>>();
assert_sync::<Configurator<FullSync>>();
assert_send::<Configurator<AccountSync>>();
}
#[test]
fn built_in_configuration_works_in_every_sync_mode() {
let local = build_local_engine(5);
local
.configure()
.rate_limit::<RateLimitPolicyError>(RateLimitPolicy::<NoLocking>::NAME, |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1,
window: Duration::from_secs(60),
},
}))
})
.expect("LocalSync retune must publish");
local
.execute_pre_trade(order(0))
.expect("first LocalSync order must pass");
assert!(local.execute_pre_trade(order(1)).is_err());
let account = build_account_engine(5);
account
.configure()
.rate_limit::<RateLimitPolicyError>(RateLimitPolicy::<AccountLocking>::NAME, |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1,
window: Duration::from_secs(60),
},
}))
})
.expect("AccountSync retune must publish");
account
.execute_pre_trade(order(0))
.expect("first AccountSync order must pass");
assert!(account.execute_pre_trade(order(1)).is_err());
let full = build_engine(5);
full.configure()
.rate_limit::<RateLimitPolicyError>(RateLimitPolicy::<FullLocking>::NAME, |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1,
window: Duration::from_secs(60),
},
}))
})
.expect("FullSync retune must publish");
full.execute_pre_trade(order(0))
.expect("first FullSync order must pass");
assert!(full.execute_pre_trade(order(1)).is_err());
}
#[test]
fn nested_configuration_is_rejected_per_engine_without_poisoning_other_engines() {
let engine = build_engine(5);
let other = build_engine(5);
let name = RateLimitPolicy::<FullLocking>::NAME;
let mut nested_error = None;
let outer = engine
.configure()
.rate_limit::<ConfigureError>(name, |settings| {
nested_error = Some(
engine
.configure()
.rate_limit::<RateLimitPolicyError>(name, |nested_settings| {
nested_settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1,
window: Duration::from_secs(60),
},
}))
})
.expect_err("same-engine nested configure must be rejected"),
);
other
.configure()
.rate_limit::<RateLimitPolicyError>(name, |other_settings| {
other_settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1,
window: Duration::from_secs(60),
},
}))
})?;
settings
.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 2,
window: Duration::from_secs(60),
},
}))
.expect("valid outer retune");
Ok(())
});
assert!(
matches!(nested_error, Some(ConfigureError::NestedConfiguration)),
"expected NestedConfiguration, got {nested_error:?}"
);
assert!(outer.is_ok(), "outer configure must publish: {outer:?}");
assert!(
other.execute_pre_trade(order(0)).is_ok(),
"first order on other engine must pass"
);
assert!(
other.execute_pre_trade(order(1)).is_err(),
"other engine retune inside callback must take effect"
);
}
#[test]
fn configure_dispatches_by_policy_name_in_multi_policy_engine() {
let builder = Engine::builder::<OrderOperation, (), ()>().full_sync();
let rate_limit =
RateLimitPolicy::<FullLocking>::new(broker_settings(100), builder.storage_builder());
let order_size = OrderSizeLimitPolicy::<FullLocking>::new(
OrderSizeLimitSettings::new(
None,
[OrderSizeAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("100")
.expect("quantity literal must be valid"),
max_notional: Volume::from_str("100000").expect("volume literal must be valid"),
},
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
}],
[],
)
.expect("settings must be valid"),
);
let engine = builder
.pre_trade(rate_limit)
.pre_trade(order_size)
.build()
.expect("engine must build");
engine
.configure()
.order_size_limit::<OrderSizeLimitPolicyError>(
OrderSizeLimitPolicy::<FullLocking>::NAME,
|settings| {
settings.set_asset_barriers([OrderSizeAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("10")
.expect("quantity literal must be valid"),
max_notional: Volume::from_str("100000")
.expect("volume literal must be valid"),
},
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
}])
},
)
.expect("order-size retune must dispatch to the named policy");
let rejects = engine
.execute_pre_trade(order_with_price(1, "15", "100"))
.err()
.expect("order must be rejected by the retuned order-size policy");
assert_eq!(rejects[0].reason, "order quantity exceeded");
let mismatch = engine
.configure()
.rate_limit::<RateLimitPolicyError>(
OrderSizeLimitPolicy::<FullLocking>::NAME,
|_settings| Ok(()),
)
.expect_err("same engine must still reject wrong typed dispatch");
assert!(
matches!(mismatch, ConfigureError::PolicyTypeMismatch { .. }),
"expected PolicyTypeMismatch, got {mismatch:?}"
);
}
#[test]
fn custom_policy_needs_no_configuration_plumbing() {
Engine::builder::<OrderOperation, (), ()>()
.no_sync()
.pre_trade(CustomPolicy)
.build()
.expect("custom policy must build without configuration hooks");
}
type PnlReport = WithExecutionReportOperation<WithFinancialImpact<()>>;
fn pnl_report(account: u64, settlement: &str, pnl: &str, fee: &str) -> PnlReport {
WithExecutionReportOperation {
inner: WithFinancialImpact {
inner: (),
financial_impact: FinancialImpact {
pnl: Pnl::from_str(pnl).expect("pnl literal must be valid"),
fee: Fee::from_str(fee).expect("fee literal must be valid"),
},
},
operation: ExecutionReportOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new(settlement).expect("asset code must be valid"),
),
account_id: AccountId::from_u64(account),
side: Side::Buy,
},
}
}
fn build_pnl_engine(
account_barriers: impl IntoIterator<Item = PnlBoundsAccountAssetBarrier>,
) -> FullSyncEngine<OrderOperation, PnlReport> {
let builder = Engine::builder::<OrderOperation, PnlReport, ()>().full_sync();
let settings =
PnlBoundsKillSwitchSettings::new([], account_barriers).expect("settings must be valid");
let policy = PnlBoundsKillSwitchPolicy::<FullLocking>::new(settings, builder.storage_builder());
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
#[test]
fn configure_pnl_bounds_broker_barrier_added_at_runtime_triggers_reject() {
let engine = build_pnl_engine([PnlBoundsAccountAssetBarrier {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(Pnl::from_str("-500").expect("pnl literal must be valid")),
upper_bound: None,
},
account_id: AccountId::from_u64(2),
initial_pnl: Pnl::ZERO,
}]);
let name = PnlBoundsKillSwitchPolicy::<FullLocking>::NAME;
engine.apply_execution_report(&pnl_report(1, "USD", "-10", "0"));
engine
.execute_pre_trade(order(1))
.expect("order with no barrier must pass before configure");
engine
.configure()
.pnl_bounds_killswitch::<PnlBoundsKillSwitchPolicyError>(name, |settings| {
settings.set_broker_barriers([PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(Pnl::from_str("-1").expect("pnl literal must be valid")),
upper_bound: None,
}])
})
.expect("adding broker barrier must publish");
let rejects = engine
.execute_pre_trade(order(1))
.err()
.expect("order past broker barrier must be rejected");
assert_eq!(
rejects[0].reason,
"pnl kill switch triggered: broker barrier"
);
}
#[test]
fn configure_pnl_bounds_pre_barrier_accumulation_seen_by_new_account_asset_barrier() {
let engine = build_pnl_engine([PnlBoundsAccountAssetBarrier {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(Pnl::from_str("-500").expect("pnl literal must be valid")),
upper_bound: None,
},
account_id: AccountId::from_u64(2),
initial_pnl: Pnl::ZERO,
}]);
let name = PnlBoundsKillSwitchPolicy::<FullLocking>::NAME;
engine.apply_execution_report(&pnl_report(1, "USD", "-80", "0"));
engine.apply_execution_report(&pnl_report(1, "USD", "-40", "0"));
engine
.execute_pre_trade(order(1))
.expect("no barrier yet: order must pass");
engine
.configure()
.pnl_bounds_killswitch::<PnlBoundsKillSwitchPolicyError>(name, |settings| {
settings.set_account_barriers([
PnlBoundsAccountAssetBarrierUpdate {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(
Pnl::from_str("-500").expect("pnl literal must be valid"),
),
upper_bound: None,
},
account_id: AccountId::from_u64(2),
},
PnlBoundsAccountAssetBarrierUpdate {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(
Pnl::from_str("-100").expect("pnl literal must be valid"),
),
upper_bound: None,
},
account_id: AccountId::from_u64(1),
},
])
})
.expect("barrier add must publish");
let rejects = engine
.execute_pre_trade(order(1))
.err()
.expect("order must be rejected by the newly-configured barrier");
assert_eq!(
rejects[0].reason,
"pnl kill switch triggered: account + asset barrier"
);
}
#[test]
fn configure_set_account_pnl_overrides_accumulated_pnl() {
let engine = build_pnl_engine([
PnlBoundsAccountAssetBarrier {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(Pnl::from_str("-100").expect("pnl literal must be valid")),
upper_bound: None,
},
account_id: AccountId::from_u64(1),
initial_pnl: Pnl::ZERO,
},
PnlBoundsAccountAssetBarrier {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(Pnl::from_str("-100").expect("pnl literal must be valid")),
upper_bound: None,
},
account_id: AccountId::from_u64(2),
initial_pnl: Pnl::ZERO,
},
]);
let name = PnlBoundsKillSwitchPolicy::<FullLocking>::NAME;
let usd = Asset::new("USD").expect("asset code must be valid");
engine
.execute_pre_trade(order(1))
.expect("order must pass at zero P&L")
.rollback();
engine
.configure()
.set_account_pnl(
name,
AccountId::from_u64(1),
usd.clone(),
Pnl::from_str("-150").expect("pnl literal must be valid"),
)
.expect("force-set must publish");
let rejects = engine
.execute_pre_trade(order(1))
.err()
.expect("order must be rejected after the override breaches the bound");
assert_eq!(
rejects[0].reason,
"pnl kill switch triggered: account + asset barrier"
);
engine
.configure()
.set_account_pnl(
name,
AccountId::from_u64(2),
usd,
Pnl::from_str("-10").expect("pnl literal must be valid"),
)
.expect("force-set inside bounds must publish");
engine
.execute_pre_trade(order(2))
.expect("order must pass after the accumulator is set inside bounds")
.rollback();
}
#[test]
fn configure_set_account_pnl_creates_absent_entry() {
let engine = build_pnl_engine([PnlBoundsAccountAssetBarrier {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(Pnl::from_str("-100").expect("pnl literal must be valid")),
upper_bound: None,
},
account_id: AccountId::from_u64(2),
initial_pnl: Pnl::ZERO,
}]);
let name = PnlBoundsKillSwitchPolicy::<FullLocking>::NAME;
engine
.configure()
.set_account_pnl(
name,
AccountId::from_u64(2),
Asset::new("USD").expect("asset code must be valid"),
Pnl::from_str("-150").expect("pnl literal must be valid"),
)
.expect("force-set on an absent entry must publish");
let rejects = engine
.execute_pre_trade(order(2))
.err()
.expect("order must be rejected after the created entry breaches the bound");
assert_eq!(
rejects[0].reason,
"pnl kill switch triggered: account + asset barrier"
);
}
#[test]
fn configure_set_account_pnl_unknown_policy_name_is_reported() {
let engine = build_pnl_engine([PnlBoundsAccountAssetBarrier {
barrier: PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
lower_bound: Some(Pnl::from_str("-100").expect("pnl literal must be valid")),
upper_bound: None,
},
account_id: AccountId::from_u64(1),
initial_pnl: Pnl::ZERO,
}]);
let error = engine
.configure()
.set_account_pnl(
"does-not-exist",
AccountId::from_u64(1),
Asset::new("USD").expect("asset code must be valid"),
Pnl::ZERO,
)
.expect_err("an unknown policy name must be rejected");
assert_eq!(
error,
ConfigureError::UnknownPolicy {
name: "does-not-exist".to_owned(),
}
);
}
#[test]
fn configure_set_account_pnl_type_mismatch_is_reported() {
let engine = build_engine(5);
let name = RateLimitPolicy::<FullLocking>::NAME;
let error = engine
.configure()
.set_account_pnl(
name,
AccountId::from_u64(1),
Asset::new("USD").expect("asset code must be valid"),
Pnl::ZERO,
)
.expect_err("targeting the wrong policy type must fail");
assert!(
matches!(&error, ConfigureError::PolicyTypeMismatch { name: n, .. } if n.as_str() == name),
"expected PolicyTypeMismatch, got {error:?}"
);
}
fn order_with_price(account: u64, quantity: &str, price: &str) -> OrderOperation {
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(account),
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 build_order_size_engine(
account_id: u64,
max_quantity: &str,
max_notional: &str,
) -> FullSyncEngine<OrderOperation> {
let builder = Engine::builder::<OrderOperation, (), ()>().full_sync();
let settings = OrderSizeLimitSettings::new(
None,
[],
[OrderSizeAccountAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str(max_quantity)
.expect("max_quantity literal must be valid"),
max_notional: Volume::from_str(max_notional)
.expect("max_notional literal must be valid"),
},
account_id: AccountId::from_u64(account_id),
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
}],
)
.expect("settings must be valid");
let policy = OrderSizeLimitPolicy::<FullLocking>::new(settings);
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
#[test]
fn configure_order_size_limit_account_asset_barrier_tightened_rejects_previously_admitted_order() {
let engine = build_order_size_engine(1, "20", "100000");
let name = OrderSizeLimitPolicy::<FullLocking>::NAME;
engine
.execute_pre_trade(order_with_price(1, "15", "100"))
.expect("order within the generous limit must pass");
engine
.configure()
.order_size_limit::<OrderSizeLimitPolicyError>(name, |settings| {
settings.set_account_asset_barriers([OrderSizeAccountAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("10")
.expect("max_quantity literal must be valid"),
max_notional: Volume::from_str("100000")
.expect("max_notional literal must be valid"),
},
account_id: AccountId::from_u64(1),
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
}])
})
.expect("tightening barrier must publish");
let rejects = engine
.execute_pre_trade(order_with_price(1, "15", "100"))
.err()
.expect("order beyond the tightened limit must be rejected");
assert_eq!(rejects[0].reason, "order quantity exceeded");
assert!(rejects[0].details.contains("max allowed: 10"));
engine
.execute_pre_trade(order_with_price(2, "15", "100"))
.expect("account 2 with no barrier must still pass");
}
#[test]
fn configure_order_size_limit_broker_barrier_added_rejects_all_accounts() {
let engine = build_order_size_engine(1, "100", "100000");
let name = OrderSizeLimitPolicy::<FullLocking>::NAME;
engine
.execute_pre_trade(order_with_price(2, "15", "100"))
.expect("account without account+asset barrier must pass before broker limit");
engine
.configure()
.order_size_limit::<OrderSizeLimitPolicyError>(name, |settings| {
settings.set_broker(Some(OrderSizeBrokerBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("10")
.expect("max_quantity literal must be valid"),
max_notional: Volume::from_str("100000")
.expect("max_notional literal must be valid"),
},
}))
})
.expect("broker barrier add must publish");
let rejects = engine
.execute_pre_trade(order_with_price(2, "15", "100"))
.err()
.expect("broker limit must reject every account");
assert_eq!(rejects[0].reason, "order quantity exceeded");
}
#[test]
fn configure_order_size_limit_asset_barrier_added_rejects_matching_settlement() {
let engine = build_order_size_engine(1, "100", "100000");
let name = OrderSizeLimitPolicy::<FullLocking>::NAME;
engine
.execute_pre_trade(order_with_price(2, "15", "100"))
.expect("account without account+asset barrier must pass before asset limit");
engine
.configure()
.order_size_limit::<OrderSizeLimitPolicyError>(name, |settings| {
settings.set_asset_barriers([OrderSizeAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("10")
.expect("max_quantity literal must be valid"),
max_notional: Volume::from_str("100000")
.expect("max_notional literal must be valid"),
},
settlement_asset: Asset::new("USD").expect("asset code must be valid"),
}])
})
.expect("asset barrier add must publish");
let rejects = engine
.execute_pre_trade(order_with_price(2, "15", "100"))
.err()
.expect("USD asset limit must reject matching settlement");
assert_eq!(rejects[0].reason, "order quantity exceeded");
}
struct SpotAdjustment {
asset: Asset,
balance: Option<AdjustmentAmount>,
balance_average_entry_price: Option<Price>,
balance_realized_pnl: Option<Pnl>,
}
impl HasBalanceAsset for SpotAdjustment {
fn balance_asset(&self) -> Result<&Asset, RequestFieldAccessError> {
Ok(&self.asset)
}
}
impl HasAccountAdjustmentBalance for SpotAdjustment {
fn balance(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(self.balance)
}
}
impl HasAccountAdjustmentBalanceAverageEntryPrice for SpotAdjustment {
fn balance_average_entry_price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(self.balance_average_entry_price)
}
}
impl HasAccountAdjustmentBalanceRealizedPnl for SpotAdjustment {
fn balance_realized_pnl(&self) -> Result<Option<Pnl>, RequestFieldAccessError> {
Ok(self.balance_realized_pnl)
}
}
impl HasAccountAdjustmentBalanceLowerBound for SpotAdjustment {
fn balance_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentBalanceUpperBound for SpotAdjustment {
fn balance_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentHeld for SpotAdjustment {
fn held(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentHeldLowerBound for SpotAdjustment {
fn held_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentHeldUpperBound for SpotAdjustment {
fn held_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncoming for SpotAdjustment {
fn incoming(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncomingLowerBound for SpotAdjustment {
fn incoming_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncomingUpperBound for SpotAdjustment {
fn incoming_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
fn spot_balance(asset_code: &str, amount: &str) -> SpotAdjustment {
SpotAdjustment {
asset: Asset::new(asset_code).expect("asset code must be valid"),
balance: Some(AdjustmentAmount::Absolute(
PositionSize::from_str(amount).expect("position size literal must be valid"),
)),
balance_average_entry_price: None,
balance_realized_pnl: None,
}
}
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotEngine = FullSyncEngine<OrderOperation, SpotReport, SpotAdjustment>;
fn build_spot_engine(slippage_bps: u16) -> SpotEngine {
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let settings = SpotFundsSettings::new(slippage_bps, SpotFundsPricingSource::Mark, [])
.expect("settings must be valid");
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
settings,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
fn build_spot_market_engine(slippage_bps: u16, mark: &str) -> (SpotEngine, InstrumentId) {
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let service = MarketDataBuilder::<FullSync>::new(QuoteTtl::Infinite).build();
let instrument = Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
);
let instrument_id = service
.register(instrument)
.expect("instrument registration must succeed");
service
.push(
instrument_id,
Quote::new().with_mark(Price::from_str(mark).expect("price literal must be valid")),
)
.expect("quote push must succeed");
let settings = SpotFundsSettings::new(slippage_bps, SpotFundsPricingSource::Mark, [])
.expect("settings must be valid");
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
settings,
Some(SpotFundsMarketData::new(Arc::clone(&service))),
builder.storage_builder(),
);
let engine = builder
.pre_trade(policy)
.build()
.expect("engine must build");
(engine, instrument_id)
}
fn seed_spot(engine: &SpotEngine, account: u64, asset_code: &str, amount: &str) {
let adj = spot_balance(asset_code, amount);
engine
.apply_account_adjustment(AccountId::from_u64(account), &[adj])
.expect("seed must succeed");
}
fn assert_market_buy_locks_price(engine: &SpotEngine, account: u64, expected: &str) {
let mut reservation = engine
.execute_pre_trade(order(account))
.expect("market buy must pass with seeded funds");
let lock_price = reservation
.lock()
.prices_of(DEFAULT_POLICY_GROUP_ID)
.next()
.expect("market buy must record a lock price");
reservation.rollback();
assert_eq!(
lock_price,
Price::from_str(expected).expect("price literal must be valid")
);
}
#[test]
fn configure_spot_funds_settings_retune_takes_effect() {
let engine = build_spot_engine(0);
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
let account_id = 1u64;
seed_spot(&engine, account_id, "USD", "1000");
engine
.execute_pre_trade(order_with_price(account_id, "5", "100"))
.expect("order within funded balance must pass")
.rollback();
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_pricing_source(SpotFundsPricingSource::BookTop);
Ok(())
})
.expect("pricing-source retune must publish");
engine
.execute_pre_trade(order_with_price(account_id, "5", "100"))
.expect("limit order must pass after pricing-source retune")
.rollback();
let validation_err = engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_global_slippage_bps(20_000)
})
.expect_err("out-of-range slippage must be rejected");
assert!(
matches!(&validation_err, ConfigureError::Validation { .. }),
"expected Validation error, got {validation_err:?}"
);
engine
.execute_pre_trade(order_with_price(account_id, "5", "100"))
.expect("engine must remain functional after failed retune")
.rollback();
}
#[test]
fn configure_spot_funds_global_slippage_retune_changes_market_lock_price() {
let (engine, _instrument_id) = build_spot_market_engine(0, "100");
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
let account_id = 1u64;
seed_spot(&engine, account_id, "USD", "1000");
assert_market_buy_locks_price(&engine, account_id, "100");
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| settings.set_global_slippage_bps(1000))
.expect("global slippage retune must publish");
assert_market_buy_locks_price(&engine, account_id, "110");
}
#[test]
fn configure_spot_funds_override_retune_changes_market_lock_price() {
let (engine, instrument_id) = build_spot_market_engine(0, "100");
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
let account_id = 1u64;
seed_spot(&engine, account_id, "USD", "1000");
assert_market_buy_locks_price(&engine, account_id, "100");
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_override(
SpotFundsOverrideTarget::Instrument(instrument_id),
SpotFundsOverride {
slippage_bps: Some(2500),
},
)
})
.expect("instrument override retune must publish");
assert_market_buy_locks_price(&engine, account_id, "125");
}