use std::cell::RefCell;
use std::rc::Rc;
use openpit::param::{AccountId, Asset, Fee, Pnl, Price, Quantity, Side, TradeAmount, Volume};
use openpit::pretrade::policies::OrderValidationPolicy;
use openpit::pretrade::{
AccountBlock, PolicyAccountAdjustmentResult, PolicyPreTradeResult, PostTradeContext,
PreTradeContext, PreTradePolicy, Reject, RejectCode, RejectScope, Rejects,
};
use openpit::{
AccountAdjustmentContext, Engine, ExecutionReportOperation, FinancialImpact, HasOrderPrice,
HasTradeAmount, Instrument, Mutation, Mutations, OrderOperation, WithExecutionReportOperation,
WithFinancialImpact,
};
type PitExecutionReport = WithExecutionReportOperation<WithFinancialImpact<()>>;
fn aapl_usd_order(quantity: &str, price: &str) -> OrderOperation {
OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("AAPL must be valid"),
Asset::new("USD").expect("USD must be valid"),
),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
trade_amount: TradeAmount::Quantity(
Quantity::from_str(quantity).expect("quantity must be valid"),
),
price: Some(Price::from_str(price).expect("price must be valid")),
}
}
#[allow(dead_code)]
fn aapl_usd_report(pnl: &str, fee: &str) -> PitExecutionReport {
PitExecutionReport {
inner: WithFinancialImpact {
inner: (),
financial_impact: FinancialImpact {
pnl: Pnl::from_str(pnl).expect("pnl must be valid"),
fee: Fee::from_str(fee).expect("fee must be valid"),
},
},
operation: ExecutionReportOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("AAPL must be valid"),
Asset::new("USD").expect("USD must be valid"),
),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
},
}
}
struct ReserveThenValidatePolicy {
reserved: Rc<RefCell<Volume>>,
next: Volume,
limit: Volume,
}
impl<O, R, A, Sync> PreTradePolicy<O, R, A, Sync> for ReserveThenValidatePolicy
where
Sync: openpit::SyncMode,
{
fn name(&self) -> &str {
"ReserveThenValidatePolicy"
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_order: &O,
mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
let prev = *self.reserved.borrow();
let rollback_reserved = Rc::clone(&self.reserved);
let next = self.next;
*self.reserved.borrow_mut() = next;
mutations.push(Mutation::new(
|| {
},
move || {
*rollback_reserved.borrow_mut() = prev;
},
));
if next > self.limit {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::RiskLimitExceeded,
"temporary reservation exceeds limit",
format!("reserved {}, limit: {}", next, self.limit),
)));
}
Ok(None)
}
fn apply_execution_report(
&self,
_ctx: &PostTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_report: &R,
) -> Option<openpit::PostTradeResult> {
None
}
}
struct NotionalCapPolicy {
max_abs_notional: Volume,
}
impl<O, R, A, Sync> PreTradePolicy<O, R, A, Sync> for NotionalCapPolicy
where
O: HasTradeAmount + HasOrderPrice,
Sync: openpit::SyncMode,
{
fn name(&self) -> &str {
"NotionalCapPolicy"
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
order: &O,
_mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
let trade_amount = match order.trade_amount() {
Ok(trade_amount) => trade_amount,
Err(error) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::MissingRequiredField,
"required order field missing",
error.to_string(),
)));
}
};
let price = match order.price() {
Ok(price) => price,
Err(error) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::MissingRequiredField,
"required order field missing",
error.to_string(),
)));
}
};
let requested_notional = match (trade_amount, price) {
(TradeAmount::Volume(volume), _) => volume,
(TradeAmount::Quantity(quantity), Some(price)) => {
match price.calculate_volume(quantity) {
Ok(v) => v,
Err(_) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::OrderValueCalculationFailed,
"order value calculation failed",
"price and quantity could not be used to evaluate notional",
)));
}
}
}
(TradeAmount::Quantity(_), None) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::OrderValueCalculationFailed,
"order value calculation failed",
"price not provided for evaluating cash flow/notional/volume",
)));
}
_ => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::UnsupportedOrderType,
"unsupported order type",
"custom trade amount variant is not supported by this policy",
)));
}
};
if requested_notional > self.max_abs_notional {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::RiskLimitExceeded,
"strategy cap exceeded",
format!(
"requested notional {}, max allowed: {}",
requested_notional, self.max_abs_notional
),
)));
}
Ok(None)
}
fn apply_execution_report(
&self,
_ctx: &PostTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_report: &R,
) -> Option<openpit::PostTradeResult> {
None
}
}
struct BlockOnAdjustmentPolicy;
impl<Order, ExecutionReport, AccountAdjustment, Sync>
PreTradePolicy<Order, ExecutionReport, AccountAdjustment, Sync> for BlockOnAdjustmentPolicy
where
Sync: openpit::SyncMode,
{
fn name(&self) -> &str {
"BlockOnAdjustmentPolicy"
}
fn apply_account_adjustment(
&self,
_ctx: &AccountAdjustmentContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_account_id: AccountId,
_adjustment: &AccountAdjustment,
_mutations: &mut Mutations,
) -> Result<PolicyAccountAdjustmentResult, Rejects> {
Ok(PolicyAccountAdjustmentResult {
account_adjustments: Vec::new(),
account_blocks: vec![AccountBlock::new(
"BlockOnAdjustmentPolicy",
RejectCode::AccountBlocked,
"blocked by account-adjustment policy",
"custom policy reported an account block from a callback",
)],
})
}
}
#[test]
fn example_wiki_domain_types_create_validated_values() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{Asset, Pnl, Price, Quantity};
let asset = Asset::new("AAPL").expect("asset code must be valid");
let quantity = Quantity::from_str("10.5").expect("quantity must be valid");
let price = Price::from_str("185").expect("price must be valid");
let pnl = Pnl::from_str("-12.5").expect("pnl must be valid");
assert_eq!(asset.as_ref(), "AAPL");
assert_eq!(quantity.to_string(), "10.5");
assert_eq!(price.to_string(), "185");
assert_eq!(pnl.to_string(), "-12.5");
Ok(())
}
#[test]
fn example_wiki_domain_types_directional_types() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{PositionSide, Side};
assert_eq!(Side::Buy.opposite(), Side::Sell);
assert_eq!(Side::Sell.sign(), -1);
assert_eq!(PositionSide::Long.opposite(), PositionSide::Short);
Ok(())
}
#[test]
fn example_wiki_domain_types_leverage() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::Leverage;
let from_multiplier = Leverage::from_u16(100).expect("valid leverage");
let from_float = Leverage::from_f64(100.5).expect("valid leverage");
assert_eq!(from_multiplier.value(), 100.0);
assert_eq!(from_float.value(), 100.5);
Ok(())
}
#[test]
fn example_wiki_getting_started_build_engine() -> Result<(), Box<dyn std::error::Error>> {
use std::time::Duration;
use openpit::param::{AccountId, Asset, Fee, Pnl, Price, Quantity, Side, TradeAmount, Volume};
use openpit::pretrade::policies::{
OrderSizeAssetBarrier, OrderSizeBrokerBarrier, OrderSizeLimit, OrderSizeLimitPolicy,
OrderSizeLimitSettings, OrderValidationPolicy, PnlBoundsBrokerBarrier,
PnlBoundsKillSwitchPolicy, PnlBoundsKillSwitchSettings, RateLimit, RateLimitBrokerBarrier,
RateLimitPolicy, RateLimitSettings,
};
use openpit::storage::NoLocking;
use openpit::{
Engine, ExecutionReportOperation, FinancialImpact, Instrument, OrderOperation,
WithExecutionReportOperation, WithFinancialImpact,
};
let usd = Asset::new("USD")?;
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let pnl_policy = PnlBoundsKillSwitchPolicy::new(
PnlBoundsKillSwitchSettings::new(
[PnlBoundsBrokerBarrier {
settlement_asset: usd.clone(),
lower_bound: Some(Pnl::from_str("-1000")?),
upper_bound: None,
}],
[],
)?,
builder.storage_builder(),
);
let rate_limit_policy = RateLimitPolicy::new(
RateLimitSettings::new(
Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 100,
window: Duration::from_secs(1),
},
}),
[],
[],
[],
)?,
builder.storage_builder(),
);
let engine = builder
.pre_trade(OrderValidationPolicy::new())
.pre_trade(pnl_policy)
.pre_trade(rate_limit_policy)
.pre_trade(OrderSizeLimitPolicy::<NoLocking>::new(
OrderSizeLimitSettings::new(
Some(OrderSizeBrokerBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("500")?,
max_notional: Volume::from_str("100000")?,
},
}),
[OrderSizeAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("500")?,
max_notional: Volume::from_str("100000")?,
},
settlement_asset: usd.clone(),
}],
[],
)?,
))
.build()?;
let order = OrderOperation {
instrument: Instrument::new(Asset::new("AAPL")?, usd.clone()),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
trade_amount: TradeAmount::Quantity(Quantity::from_f64(100.0)?),
price: Some(Price::from_str("185")?),
};
let request = engine.start_pre_trade(order).map_err(|rejects| {
let message = rejects
.iter()
.map(|r| {
format!(
"rejected by {} [{}]: {} ({})",
r.policy, r.code, r.reason, r.details,
)
})
.collect::<Vec<_>>()
.join(", ");
std::io::Error::other(message)
})?;
let mut reservation = request.execute().map_err(|rejects| {
let message = rejects
.iter()
.map(|r| {
format!(
"rejected by {} [{}]: {} ({})",
r.policy, r.code, r.reason, r.details,
)
})
.collect::<Vec<_>>()
.join(", ");
std::io::Error::other(message)
})?;
reservation.commit();
let report = WithExecutionReportOperation {
inner: WithFinancialImpact {
inner: (),
financial_impact: FinancialImpact {
pnl: Pnl::from_str("-50")?,
fee: Fee::from_str("3.4")?,
},
},
operation: ExecutionReportOperation {
instrument: Instrument::new(Asset::new("AAPL")?, usd),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
},
};
let result = engine.apply_execution_report(&report);
for outcome in &result.account_pnls {
eprintln!("account P&L outcome for {}", outcome.account_id);
}
for outcome in &result.account_adjustments {
eprintln!(
"account adjustment from group {}",
outcome.policy_group_id.value()
);
}
assert!(result.account_blocks.is_empty());
Ok(())
}
#[test]
fn example_wiki_pipeline_start_stage_reject() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let order = aapl_usd_order("100", "185");
match engine.start_pre_trade(order) {
Ok(request) => {
let _request = request;
}
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy, reject.code, reject.reason, reject.details
);
}
}
}
Ok(())
}
#[test]
fn example_wiki_pipeline_main_stage_finalize() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let order = aapl_usd_order("100", "185");
let request = engine
.start_pre_trade(order)
.expect("start stage must pass");
match request.execute() {
Ok(mut reservation) => {
reservation.commit()
}
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy, reject.code, reject.reason, reject.details
);
}
}
}
Ok(())
}
#[test]
fn example_wiki_getting_started_run_order() {
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()
.expect("engine must build");
let order = aapl_usd_order("100", "185");
let request = match engine.start_pre_trade(order) {
Ok(request) => request,
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy, reject.code, reject.reason, reject.details
);
}
return;
}
};
let mut reservation = match request.execute() {
Ok(reservation) => reservation,
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy, reject.code, reject.reason, reject.details
);
}
return;
}
};
reservation.commit();
}
#[test]
fn example_wiki_pipeline_shortcut_start_and_main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let order = aapl_usd_order("100", "185");
match engine.execute_pre_trade(order) {
Ok(mut reservation) => {
reservation.commit()
}
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy, reject.code, reject.reason, reject.details
);
}
}
}
Ok(())
}
#[test]
fn example_wiki_pipeline_apply_post_trade_feedback() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let report = aapl_usd_report("-50", "3.4");
let result = engine.apply_execution_report(&report);
for outcome in &result.account_pnls {
eprintln!("account P&L outcome for {}", outcome.account_id);
}
for outcome in &result.account_adjustments {
eprintln!(
"account adjustment from group {}",
outcome.policy_group_id.value()
);
}
if !result.account_blocks.is_empty() {
eprintln!("halt new orders until the blocked state is cleared");
}
assert!(result.account_blocks.is_empty());
Ok(())
}
#[test]
fn example_wiki_account_adjustments() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AdjustmentAmount, PositionMode, PositionSize};
use openpit::{
AccountAdjustmentAmount, AccountAdjustmentBalanceOperation,
AccountAdjustmentPositionOperation, Engine, Instrument,
};
#[derive(Clone)]
#[allow(dead_code)]
enum AccountAdjustmentOperation {
Balance(AccountAdjustmentBalanceOperation),
Position(AccountAdjustmentPositionOperation),
}
#[derive(Clone)]
#[allow(dead_code)]
struct AccountAdjustment {
operation: AccountAdjustmentOperation,
amount: AccountAdjustmentAmount,
}
let account_id = AccountId::from_u64(99224416);
let adjustments = vec![
AccountAdjustment {
operation: AccountAdjustmentOperation::Balance(AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
}),
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_f64(10000.0)?)),
held: None,
incoming: None,
},
},
AccountAdjustment {
operation: AccountAdjustmentOperation::Position(AccountAdjustmentPositionOperation {
instrument: Instrument::new(Asset::new("SPX")?, Asset::new("USD")?),
collateral_asset: Asset::new("USD")?,
average_entry_price: Price::from_f64(95000.0)?,
mode: PositionMode::Hedged,
leverage: None,
}),
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_f64(-3.0)?)),
held: None,
incoming: None,
},
},
];
struct AcceptAllAdjustments;
impl<Sync> openpit::pretrade::PreTradePolicy<(), (), AccountAdjustment, Sync>
for AcceptAllAdjustments
where
Sync: openpit::SyncMode,
{
fn name(&self) -> &'static str {
"AcceptAllAdjustments"
}
fn apply_account_adjustment(
&self,
_ctx: &openpit::AccountAdjustmentContext<
<Sync as openpit::SyncMode>::StorageLockingPolicyFactory,
>,
_account_id: openpit::param::AccountId,
_adjustment: &AccountAdjustment,
_mutations: &mut openpit::Mutations,
) -> Result<openpit::pretrade::PolicyAccountAdjustmentResult, openpit::pretrade::Rejects>
{
Ok(openpit::pretrade::PolicyAccountAdjustmentResult {
account_adjustments: Vec::new(),
account_blocks: Vec::new(),
})
}
}
let engine = Engine::builder::<(), (), AccountAdjustment>()
.no_sync()
.pre_trade(AcceptAllAdjustments)
.build()?;
let result = engine.apply_account_adjustment(account_id, &adjustments)?;
assert!(result.account_blocks.is_empty());
Ok(())
}
#[test]
fn example_wiki_account_adjustments_balance_limit_policy() -> Result<(), Box<dyn std::error::Error>>
{
use std::sync::Arc;
use openpit::storage::{CreateStorageFor, LockingPolicyFactory, Storage, StorageBuilder};
trait HasAssetDelta {
fn asset_id(&self) -> &str;
fn delta(&self) -> Volume;
}
struct BalanceLimitPolicy<StorageLockingPolicyFactory>
where
StorageLockingPolicyFactory: LockingPolicyFactory,
{
max_total: Volume,
totals: Arc<Storage<String, Volume, StorageLockingPolicyFactory::Policy>>,
}
impl<StorageLockingPolicyFactory> BalanceLimitPolicy<StorageLockingPolicyFactory>
where
StorageLockingPolicyFactory: LockingPolicyFactory + CreateStorageFor<String>,
{
fn new(
max_total: Volume,
storage_builder: &StorageBuilder<StorageLockingPolicyFactory>,
) -> Self {
Self {
max_total,
totals: Arc::new(storage_builder.create_for_bound_key()),
}
}
}
impl<Order, ExecutionReport, A, Sync, StorageLockingPolicyFactory>
PreTradePolicy<Order, ExecutionReport, A, Sync>
for BalanceLimitPolicy<StorageLockingPolicyFactory>
where
A: HasAssetDelta,
Sync: openpit::SyncMode,
StorageLockingPolicyFactory: LockingPolicyFactory + CreateStorageFor<String>,
StorageLockingPolicyFactory::Policy: 'static,
{
fn name(&self) -> &str {
"BalanceLimitPolicy"
}
fn apply_account_adjustment(
&self,
_ctx: &AccountAdjustmentContext<
<Sync as openpit::SyncMode>::StorageLockingPolicyFactory,
>,
_account_id: AccountId,
adjustment: &A,
mutations: &mut Mutations,
) -> Result<PolicyAccountAdjustmentResult, Rejects> {
let asset_id = adjustment.asset_id().to_owned();
let delta = adjustment.delta();
let prev_total = self
.totals
.with(&asset_id, |total| *total)
.unwrap_or(Volume::ZERO);
let new_total = prev_total.checked_add(delta).map_err(|error| {
Rejects::from(Reject::new(
"BalanceLimitPolicy",
RejectScope::Account,
RejectCode::RiskLimitExceeded,
"invalid adjustment total",
error.to_string(),
))
})?;
if new_total > self.max_total {
return Err(Rejects::from(Reject::new(
"BalanceLimitPolicy",
RejectScope::Account,
RejectCode::RiskLimitExceeded,
"cumulative adjustment exceeds limit",
format!("asset {asset_id}: {new_total} > {}", self.max_total),
)));
}
self.totals.with_mut(
asset_id.clone(),
|| Volume::ZERO,
|entry, _is_new| {
*entry = new_total;
},
);
let rollback_totals = Arc::clone(&self.totals);
let rollback_asset = asset_id;
mutations.push(Mutation::new(
|| {
},
move || {
rollback_totals.with_mut(
rollback_asset,
|| Volume::ZERO,
|entry, _is_new| {
*entry = prev_total;
},
);
},
));
Ok(PolicyAccountAdjustmentResult {
account_adjustments: Vec::new(),
account_blocks: Vec::new(),
})
}
}
struct SimpleAdjustment {
asset: String,
delta: Volume,
}
impl HasAssetDelta for SimpleAdjustment {
fn asset_id(&self) -> &str {
&self.asset
}
fn delta(&self) -> Volume {
self.delta
}
}
let builder = Engine::builder::<(), (), SimpleAdjustment>().no_sync();
let policy = BalanceLimitPolicy::new(Volume::from_str("1000000")?, builder.storage_builder());
let engine = builder.pre_trade(policy).build()?;
let result = engine.apply_account_adjustment(
AccountId::from_u64(99224416),
&[SimpleAdjustment {
asset: "USD".to_string(),
delta: Volume::from_str("100")?,
}],
)?;
assert!(result.account_blocks.is_empty());
Ok(())
}
#[test]
fn example_wiki_policy_rollback_safety() -> Result<(), Box<dyn std::error::Error>> {
let reserved = Rc::new(RefCell::new(Volume::from_str("0")?));
let reserve_policy = ReserveThenValidatePolicy {
reserved: Rc::clone(&reserved),
next: Volume::from_str("100")?,
limit: Volume::from_str("50")?,
};
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(reserve_policy)
.build()?;
let request = engine.start_pre_trade(aapl_usd_order("10", "25"))?;
let rejects = match request.execute() {
Ok(_) => panic!("main stage must reject"),
Err(rejects) => rejects,
};
assert_eq!(rejects[0].code, RejectCode::RiskLimitExceeded);
assert_eq!(reserved.borrow().to_string(), "0");
Ok(())
}
#[test]
fn example_wiki_policy_notional_cap() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(NotionalCapPolicy {
max_abs_notional: Volume::from_str("1000")?,
})
.build()?;
let request = engine.start_pre_trade(aapl_usd_order("10", "25"))?;
request.execute()?.commit();
let request = engine.start_pre_trade(aapl_usd_order("100", "25"))?;
let rejects = match request.execute() {
Ok(_) => panic!("main stage must reject"),
Err(rejects) => rejects,
};
assert_eq!(rejects[0].code, RejectCode::RiskLimitExceeded);
Ok(())
}
#[test]
fn example_wiki_policy_blocks_account_from_adjustment() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::builder::<OrderOperation, (), ()>()
.no_sync()
.pre_trade(BlockOnAdjustmentPolicy)
.build()?;
let account = AccountId::from_u64(99224416);
let adjustment_result = engine.apply_account_adjustment(account, &[()])?;
assert_eq!(adjustment_result.account_blocks.len(), 1);
let rejects = match engine.start_pre_trade(aapl_usd_order("10", "25")) {
Ok(_) => panic!("blocked account must fail the start stage"),
Err(rejects) => rejects,
};
assert_eq!(rejects[0].code, RejectCode::AccountBlocked);
Ok(())
}
#[test]
fn example_wiki_policy_api_custom_rust_models() -> Result<(), Box<dyn std::error::Error>> {
use std::ops::Deref;
use openpit::{ExecutionReportOperation, OrderOperation};
struct StrategyOrder {
inner: OrderOperation,
strategy_tag: String,
}
impl Deref for StrategyOrder {
type Target = OrderOperation;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
struct StrategyReport {
inner: ExecutionReportOperation,
venue_exec_id: String,
}
impl Deref for StrategyReport {
type Target = ExecutionReportOperation;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
let order = StrategyOrder {
inner: aapl_usd_order("10", "25"),
strategy_tag: "alpha".to_owned(),
};
let report = StrategyReport {
inner: ExecutionReportOperation {
instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
account_id: AccountId::from_u64(99224416),
side: Side::Buy,
},
venue_exec_id: "venue-42".to_owned(),
};
assert_eq!(order.strategy_tag, "alpha");
assert_eq!(order.account_id, AccountId::from_u64(99224416));
assert_eq!(report.venue_exec_id, "venue-42");
assert_eq!(report.side, Side::Buy);
Ok(())
}
#[test]
fn example_wiki_policies_order_validation() -> Result<(), Box<dyn std::error::Error>> {
use openpit::pretrade::policies::OrderValidationPolicy;
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let order = aapl_usd_order("100", "185");
engine.start_pre_trade(order)?.execute()?.commit();
Ok(())
}
#[test]
fn example_wiki_policies_rate_limit() -> Result<(), Box<dyn std::error::Error>> {
use std::time::Duration;
use openpit::pretrade::policies::{
RateLimit, RateLimitBrokerBarrier, RateLimitPolicy, RateLimitSettings,
};
let builder = Engine::builder::<OrderOperation, PitExecutionReport, ()>().no_sync();
let policy = RateLimitPolicy::new(
RateLimitSettings::new(
Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 100,
window: Duration::from_secs(1),
},
}),
[],
[],
[],
)?,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let order = aapl_usd_order("1", "100");
engine.start_pre_trade(order)?.execute()?.commit();
Ok(())
}
#[test]
fn example_wiki_policies_order_size_limit() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{Asset, Quantity, Volume};
use openpit::pretrade::policies::{
OrderSizeAssetBarrier, OrderSizeBrokerBarrier, OrderSizeLimit, OrderSizeLimitPolicy,
OrderSizeLimitSettings,
};
use openpit::storage::NoLocking;
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderSizeLimitPolicy::<NoLocking>::new(
OrderSizeLimitSettings::new(
Some(OrderSizeBrokerBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("100")?,
max_notional: Volume::from_str("50000")?,
},
}),
[OrderSizeAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("100")?,
max_notional: Volume::from_str("50000")?,
},
settlement_asset: Asset::new("USD")?,
}],
[],
)?,
))
.build()?;
let order = aapl_usd_order("10", "100");
engine.start_pre_trade(order)?.execute()?.commit();
Ok(())
}
#[test]
fn example_wiki_policies_pnl_bounds_killswitch() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{Asset, Pnl};
use openpit::pretrade::policies::{
PnlBoundsBrokerBarrier, PnlBoundsKillSwitchPolicy, PnlBoundsKillSwitchSettings,
};
let builder = Engine::builder::<OrderOperation, PitExecutionReport, ()>().no_sync();
let policy = PnlBoundsKillSwitchPolicy::new(
PnlBoundsKillSwitchSettings::new(
[PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD")?,
lower_bound: Some(Pnl::from_str("-1000")?),
upper_bound: Some(Pnl::from_str("500")?),
}],
[],
)?,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let order = aapl_usd_order("1", "100");
engine.start_pre_trade(order)?.execute()?.commit();
Ok(())
}
#[test]
fn example_wiki_storage_custom_policy() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AccountId, Asset, Pnl};
use openpit::storage::{LockingPolicyFactory, Storage, StorageBuilder};
pub struct MyPolicy<StorageLockingPolicyFactory>
where
StorageLockingPolicyFactory: LockingPolicyFactory,
{
realized: Storage<(AccountId, Asset), Pnl, StorageLockingPolicyFactory::Policy>,
}
impl<StorageLockingPolicyFactory> MyPolicy<StorageLockingPolicyFactory>
where
StorageLockingPolicyFactory:
LockingPolicyFactory + openpit::storage::CreateStorageFor<(AccountId, Asset)>,
{
pub fn new(storage_builder: &StorageBuilder<StorageLockingPolicyFactory>) -> Self {
Self {
realized: storage_builder.create_for_bound_key(),
}
}
pub fn record_pnl(&self, account: AccountId, settlement: Asset, delta: Pnl) {
self.realized.with_mut(
(account, settlement),
|| Pnl::ZERO,
|entry, _is_new| {
if let Ok(updated) = entry.checked_add(delta) {
*entry = updated;
}
},
);
}
pub fn current_pnl(&self, account: AccountId, settlement: &Asset) -> Pnl {
let key = (account, settlement.clone());
self.realized
.with(&key, |entry| *entry)
.unwrap_or(Pnl::ZERO)
}
}
let builder = Engine::builder::<(), (), ()>().no_sync();
let policy = MyPolicy::new(builder.storage_builder());
let account = AccountId::from_u64(1);
let usd = Asset::new("USD")?;
policy.record_pnl(account, usd.clone(), Pnl::from_str("-50")?);
assert_eq!(policy.current_pnl(account, &usd), Pnl::from_str("-50")?);
Ok(())
}
#[test]
fn example_wiki_storage_engine_builder() -> Result<(), Box<dyn std::error::Error>> {
let builder = Engine::builder::<(), (), ()>().full_sync();
let counters = builder
.storage_builder()
.create_for_bound_key::<&'static str, u64>();
counters.with_mut(
"ticks",
|| 0,
|value, _is_new| {
*value += 1;
},
);
assert_eq!(counters.with(&"ticks", |value| *value), Some(1));
assert!(counters.remove(&"ticks"));
Ok(())
}
#[test]
fn example_wiki_policies_spot_funds() -> Result<(), Box<dyn std::error::Error>> {
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::{
Engine, FullSync, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let _engine = builder.pre_trade(policy).build()?;
Ok(())
}
#[test]
fn example_wiki_spot_funds_limit_only() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{
AccountId, AdjustmentAmount, Asset, PositionSize, Price, Quantity, Side, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::{
AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
Engine, FullSync, Instrument, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let account = AccountId::from_u64(99224416);
let seed = WithAccountAdjustmentAmount {
inner: WithAccountAdjustmentBounds {
inner: WithAccountAdjustmentBalanceOperation {
inner: AccountAdjustmentAmount::default(),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
},
},
bounds: AccountAdjustmentBounds::default(),
},
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("10000")?)),
held: None,
incoming: None,
},
};
engine.apply_account_adjustment(account, &[seed])?;
let order = OrderOperation {
instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
account_id: account,
side: Side::Buy,
trade_amount: TradeAmount::Quantity(Quantity::from_str("10")?),
price: Some(Price::from_str("200")?),
};
engine.execute_pre_trade(order)?.commit();
Ok(())
}
#[test]
fn example_wiki_spot_funds_market_orders() -> Result<(), Box<dyn std::error::Error>> {
use std::sync::Arc;
use openpit::param::{
AccountId, AdjustmentAmount, Asset, PositionSize, Price, Quantity, Side, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::{
AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
Engine, FullSync, Instrument, OrderOperation, Quote, QuoteTtl, SpotFundsMarketData,
SpotFundsPricingSource, WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let market_data = builder.market_data(QuoteTtl::Infinite).build();
let aapl = Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?);
let aapl_id = market_data.register(aapl.clone())?;
market_data.push(aapl_id, Quote::new().with_mark(Price::from_str("200")?))?;
let settings = SpotFundsSettings::new(1500, SpotFundsPricingSource::Mark, [])?;
let bundle = SpotFundsMarketData::new(Arc::clone(&market_data));
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
settings,
Some(bundle),
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let account = AccountId::from_u64(99224416);
let seed = WithAccountAdjustmentAmount {
inner: WithAccountAdjustmentBounds {
inner: WithAccountAdjustmentBalanceOperation {
inner: AccountAdjustmentAmount::default(),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
},
},
bounds: AccountAdjustmentBounds::default(),
},
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("10000")?)),
held: None,
incoming: None,
},
};
engine.apply_account_adjustment(account, &[seed])?;
let order = OrderOperation {
instrument: aapl,
account_id: account,
side: Side::Buy,
trade_amount: TradeAmount::Quantity(Quantity::from_str("5")?),
price: None,
};
engine.execute_pre_trade(order)?.commit();
Ok(())
}
#[test]
fn example_wiki_spot_funds_pnl_kill_switch_builder() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AccountId, Pnl};
use openpit::pretrade::policies::{
SpotFundsPnlBoundsAccountBarrier, SpotFundsPnlBoundsBarrier, SpotFundsPolicy,
};
use openpit::{
Engine, FullSync, OrderOperation, SpotFundsMarketData, WithAccountAdjustmentAmount,
WithAccountAdjustmentBalanceOperation, WithAccountAdjustmentBounds,
WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let account = AccountId::from_u64(99224416);
let global_barrier = SpotFundsPnlBoundsBarrier {
lower_bound: Some(Pnl::from_str("-1000")?),
upper_bound: None,
};
let account_barrier = SpotFundsPnlBoundsAccountBarrier {
barrier: SpotFundsPnlBoundsBarrier {
lower_bound: Some(Pnl::from_str("-250")?),
upper_bound: None,
},
account_id: account,
};
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::pnl_bounds_kill_switch(
Some(global_barrier),
[],
[account_barrier],
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
)?;
let engine = builder.pre_trade(policy).build()?;
let _ = &engine;
Ok(())
}
#[test]
fn example_wiki_spot_funds_pnl_kill_switch_reconfigure() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AccountId, Pnl};
use openpit::pretrade::policies::{
SpotFundsConfigError, SpotFundsPnlBoundsBarrier, SpotFundsPolicy, SpotFundsPricingSource,
SpotFundsSettings,
};
use openpit::{
Engine, FullSync, OrderOperation, SpotFundsMarketData, WithAccountAdjustmentAmount,
WithAccountAdjustmentBalanceOperation, WithAccountAdjustmentBounds,
WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let account = AccountId::from_u64(99224416);
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
let new_lower = Pnl::from_str("-500")?;
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_pnl_global_barrier(Some(SpotFundsPnlBoundsBarrier {
lower_bound: Some(new_lower),
upper_bound: None,
}))
})?;
let result = engine.configure().set_spot_funds_account_pnl(
name,
account,
openpit::PnlState::Value(Pnl::from_str("-600")?),
)?;
assert_eq!(result.account_blocks.len(), 1);
Ok(())
}
#[test]
fn example_wiki_balance_reconciliation_delta_absolute() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AccountId, AdjustmentAmount, Asset, PositionSize};
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::{
AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
Engine, FullSync, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let account = AccountId::from_u64(99224416);
let seed = |amount: &str| -> Result<SpotAdjustment, Box<dyn std::error::Error>> {
Ok(WithAccountAdjustmentAmount {
inner: WithAccountAdjustmentBounds {
inner: WithAccountAdjustmentBalanceOperation {
inner: AccountAdjustmentAmount::default(),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
},
},
bounds: AccountAdjustmentBounds::default(),
},
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str(amount)?)),
held: None,
incoming: None,
},
})
};
let first = engine.apply_account_adjustment(account, &[seed("10000")?])?;
let usd = first.outcomes[0].entry.balance.expect("balance changed");
assert_eq!(usd.delta, PositionSize::from_str("10000")?);
assert_eq!(usd.absolute, PositionSize::from_str("10000")?);
let second = engine.apply_account_adjustment(account, &[seed("15000")?])?;
let usd = second.outcomes[0].entry.balance.expect("balance changed");
assert_eq!(usd.delta, PositionSize::from_str("5000")?);
assert_eq!(usd.absolute, PositionSize::from_str("15000")?);
Ok(())
}
#[test]
fn example_wiki_pre_trade_lock_persistence() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{
AccountId, AdjustmentAmount, Asset, PositionSize, Price, Quantity, Side, Trade, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::pretrade::PreTradeLock;
use openpit::{
AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
Engine, ExecutionReportFillDetails, ExecutionReportOperation, FullSync, Instrument,
OrderOperation, PolicyGroupId, SpotFundsMarketData, SpotFundsPricingSource,
WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let account = AccountId::from_u64(99224416);
let instrument = Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?);
let seed = WithAccountAdjustmentAmount {
inner: WithAccountAdjustmentBounds {
inner: WithAccountAdjustmentBalanceOperation {
inner: AccountAdjustmentAmount::default(),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
},
},
bounds: AccountAdjustmentBounds::default(),
},
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("10000")?)),
held: None,
incoming: None,
},
};
engine.apply_account_adjustment(account, &[seed])?;
let order = OrderOperation {
instrument: instrument.clone(),
account_id: account,
side: Side::Buy,
trade_amount: TradeAmount::Quantity(Quantity::from_str("10")?),
price: Some(Price::from_str("200")?),
};
let mut reservation = engine.execute_pre_trade(order)?;
let persisted: Vec<(u16, String)> = reservation
.lock()
.entries()
.map(|(group, price)| (group.value(), price.to_string()))
.collect();
reservation.commit();
let restored = persisted
.iter()
.map(|(group, price)| {
Ok::<_, Box<dyn std::error::Error>>((
PolicyGroupId::new(*group),
Price::from_str(price)?,
))
})
.collect::<Result<PreTradeLock, _>>()?;
let report = WithExecutionReportOperation {
inner: WithExecutionReportFillDetails {
inner: (),
fill: ExecutionReportFillDetails {
last_trade: Some(Trade {
price: Price::from_str("200")?,
quantity: Quantity::from_str("10")?,
}),
fee: None,
leaves_quantity: Quantity::from_str("0")?,
lock: restored,
is_final: true,
},
},
operation: ExecutionReportOperation {
instrument,
account_id: account,
side: Side::Buy,
},
};
let result = engine.apply_execution_report(&report);
assert!(result.account_blocks.is_empty());
Ok(())
}
#[test]
fn example_wiki_account_groups_register_and_read() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AccountGroupId, AccountId};
use openpit::pretrade::policies::OrderValidationPolicy;
use openpit::{Engine, OrderOperation};
let engine: openpit::LocalEngine<OrderOperation> = Engine::builder()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let accounts = engine.accounts();
let hedge_book = AccountGroupId::from_u32(7)?;
accounts.register_group(
&[AccountId::from_u64(10), AccountId::from_u64(11)],
hedge_book,
)?;
assert_eq!(accounts.group_of(AccountId::from_u64(10)), Some(hedge_book));
assert_eq!(accounts.group_of(AccountId::from_u64(99)), None);
accounts.unregister_group(
&[AccountId::from_u64(10), AccountId::from_u64(11)],
hedge_book,
)?;
assert_eq!(accounts.group_of(AccountId::from_u64(10)), None);
Ok(())
}
#[test]
fn example_wiki_account_block_unblock() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AccountGroupId, AccountId};
use openpit::pretrade::policies::OrderValidationPolicy;
use openpit::{Engine, OrderOperation};
let engine: openpit::LocalEngine<OrderOperation> = Engine::builder()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let accounts = engine.accounts();
accounts.block(AccountId::from_u64(99224416), "compliance hold".to_string());
accounts.unblock(AccountId::from_u64(99224416));
let desk = AccountGroupId::from_u32(7)?;
accounts.block_group(desk, "desk suspended".to_string())?;
accounts.unblock_group(desk)?;
Ok(())
}
#[test]
fn example_wiki_spot_funds_global_limit_mode() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{
AccountId, AdjustmentAmount, Asset, PositionSize, Price, Quantity, Side, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsConfigError, SpotFundsPolicy, SpotFundsSettings};
use openpit::pretrade::SpotFundsLimitMode;
use openpit::{
AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
Engine, FullSync, Instrument, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let account = AccountId::from_u64(99224416);
let seed = WithAccountAdjustmentAmount {
inner: WithAccountAdjustmentBounds {
inner: WithAccountAdjustmentBalanceOperation {
inner: AccountAdjustmentAmount::default(),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
},
},
bounds: AccountAdjustmentBounds::default(),
},
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("1000")?)),
held: None,
incoming: None,
},
};
engine.apply_account_adjustment(account, &[seed])?;
let order = OrderOperation {
instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
account_id: account,
side: Side::Buy,
trade_amount: TradeAmount::Quantity(Quantity::from_str("10")?),
price: Some(Price::from_str("200")?),
};
let rejects = engine
.execute_pre_trade(order.clone())
.err()
.expect("enforce must reject insufficient funds");
assert_eq!(rejects[0].reason, "spot funds insufficient");
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_global_limit_mode(SpotFundsLimitMode::TrackOnly);
Ok(())
})?;
engine.execute_pre_trade(order.clone())?.commit();
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_global_limit_mode(SpotFundsLimitMode::Enforce);
Ok(())
})?;
let rejects = engine
.execute_pre_trade(order)
.err()
.expect("enforce must reject negative available");
assert_eq!(rejects[0].reason, "spot funds insufficient");
Ok(())
}
#[test]
fn example_wiki_spot_funds_per_account_limit_mode() -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{
AccountId, AdjustmentAmount, Asset, PositionSize, Price, Quantity, Side, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsConfigError, SpotFundsPolicy, SpotFundsSettings};
use openpit::pretrade::SpotFundsLimitMode;
use openpit::{
AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
Engine, FullSync, Instrument, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<
WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
let account = AccountId::from_u64(99224416);
let seed = WithAccountAdjustmentAmount {
inner: WithAccountAdjustmentBounds {
inner: WithAccountAdjustmentBalanceOperation {
inner: AccountAdjustmentAmount::default(),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
},
},
bounds: AccountAdjustmentBounds::default(),
},
amount: AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("1000")?)),
held: None,
incoming: None,
},
};
engine.apply_account_adjustment(account, &[seed])?;
let order = OrderOperation {
instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
account_id: account,
side: Side::Buy,
trade_amount: TradeAmount::Quantity(Quantity::from_str("10")?),
price: Some(Price::from_str("200")?),
};
let rejects = engine
.execute_pre_trade(order.clone())
.err()
.expect("enforce must reject insufficient funds");
assert_eq!(rejects[0].reason, "spot funds insufficient");
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_account_limit_mode(account, Some(SpotFundsLimitMode::TrackOnly));
Ok(())
})?;
engine.execute_pre_trade(order.clone())?.commit();
engine
.configure()
.spot_funds::<SpotFundsConfigError>(name, |settings| {
settings.set_account_limit_mode(account, None);
Ok(())
})?;
let rejects = engine
.execute_pre_trade(order)
.err()
.expect("enforce must reject after pin is cleared");
assert_eq!(rejects[0].reason, "spot funds insufficient");
Ok(())
}
#[test]
fn example_wiki_dynamic_policy_reconfiguration_rate_limit() -> Result<(), Box<dyn std::error::Error>>
{
use std::time::Duration;
use openpit::pretrade::policies::{
RateLimit, RateLimitBrokerBarrier, RateLimitPolicy, RateLimitPolicyError, RateLimitSettings,
};
use openpit::storage::NoLocking;
use openpit::{Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact};
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
fn order() -> OrderOperation {
aapl_usd_order("1", "100")
}
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = RateLimitPolicy::new(
RateLimitSettings::new(
Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 5,
window: Duration::from_secs(60),
},
}),
[],
[],
[],
)?,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
for _ in 0..3 {
engine.execute_pre_trade(order())?.commit();
}
let name = RateLimitPolicy::<NoLocking>::NAME;
engine
.configure()
.rate_limit::<RateLimitPolicyError>(name, |settings| {
settings.set_broker(Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 2,
window: Duration::from_secs(60),
},
}))
})?;
let rejects = engine
.execute_pre_trade(order())
.err()
.expect("order beyond the tightened limit must be rejected");
assert_eq!(rejects[0].reason, "rate limit exceeded: broker barrier");
Ok(())
}
#[test]
fn example_wiki_dynamic_policy_reconfiguration_set_account_pnl(
) -> Result<(), Box<dyn std::error::Error>> {
use openpit::param::{AccountId, Asset, Pnl};
use openpit::pretrade::policies::{
PnlBoundsBrokerBarrier, PnlBoundsKillSwitchPolicy, PnlBoundsKillSwitchSettings,
};
use openpit::storage::NoLocking;
use openpit::{Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact};
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
let account = AccountId::from_u64(99224416);
fn order() -> OrderOperation {
aapl_usd_order("1", "100")
}
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = PnlBoundsKillSwitchPolicy::new(
PnlBoundsKillSwitchSettings::new(
[PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD")?,
lower_bound: Some(Pnl::from_str("-100")?),
upper_bound: None,
}],
[],
)?,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
engine.execute_pre_trade(order())?.commit();
let name = PnlBoundsKillSwitchPolicy::<NoLocking>::NAME;
engine.configure().set_account_pnl(
name,
account,
Asset::new("USD")?,
Pnl::from_str("-150")?,
)?;
let rejects = engine
.execute_pre_trade(order())
.err()
.expect("order beyond the breached bound must be rejected");
assert_eq!(
rejects[0].reason,
"pnl kill switch triggered: broker barrier"
);
Ok(())
}
#[test]
fn example_wiki_dry_run_verdict() -> Result<(), Box<dyn std::error::Error>> {
use openpit::pretrade::policies::OrderValidationPolicy;
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let order = aapl_usd_order("100", "185");
let report = engine.execute_pre_trade_dry_run(order);
if report.is_pass() {
println!("order would be admitted");
} else {
for reject in report.rejects().unwrap().iter() {
eprintln!(
"would reject by {} [{}]: {} ({})",
reject.policy, reject.code, reject.reason, reject.details
);
}
}
assert!(report.is_pass());
Ok(())
}
#[test]
fn example_wiki_dry_run_before_real_call() -> Result<(), Box<dyn std::error::Error>> {
use openpit::pretrade::policies::OrderValidationPolicy;
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
let order = aapl_usd_order("100", "185");
let probe = engine.execute_pre_trade_dry_run(order.clone());
if probe.is_pass() {
engine.execute_pre_trade(order)?.commit();
}
Ok(())
}
struct MyCountingPolicy {
count: std::cell::Cell<u64>,
}
impl<O, R, A, Sync> PreTradePolicy<O, R, A, Sync> for MyCountingPolicy
where
Sync: openpit::SyncMode,
{
fn name(&self) -> &str {
"MyCountingPolicy"
}
fn check_pre_trade_start(
&self,
_ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_order: &O,
) -> Result<(), Rejects> {
self.count.set(self.count.get() + 1); Ok(())
}
fn check_pre_trade_start_dry_run(
&self,
_ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_order: &O,
) -> Result<(), Rejects> {
Ok(()) }
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_order: &O,
_mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
Ok(None)
}
fn apply_execution_report(
&self,
_ctx: &PostTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_report: &R,
) -> Option<openpit::PostTradeResult> {
None
}
}
#[test]
fn example_wiki_dry_run_custom_policy_hook() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(MyCountingPolicy {
count: std::cell::Cell::new(0),
})
.build()?;
let order = aapl_usd_order("10", "100");
let probe = engine.execute_pre_trade_dry_run(order.clone());
assert!(probe.is_pass());
engine.execute_pre_trade(order)?.commit();
Ok(())
}