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::{
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
}
}
#[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_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_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);
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,
realized_pnl: 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<Vec<openpit::AccountOutcomeEntry>, openpit::pretrade::Rejects> {
Ok(Vec::new())
}
}
let engine = Engine::builder()
.no_sync()
.pre_trade(AcceptAllAdjustments)
.build()?;
let result = engine.apply_account_adjustment(account_id, &adjustments);
assert!(result.is_ok());
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<Vec<openpit::AccountOutcomeEntry>, 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(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.is_ok());
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_custom_types_manual() -> Result<(), Box<dyn std::error::Error>> {
use openpit::{HasInstrument, RequestFieldAccessError};
struct MyOrder {
instrument: Instrument,
}
impl HasInstrument for MyOrder {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
let order = MyOrder {
instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
};
let instrument = order.instrument()?;
assert_eq!(instrument.settlement_asset(), &Asset::new("USD")?);
Ok(())
}
#[cfg(feature = "derive")]
#[test]
fn example_wiki_custom_types_derive() -> Result<(), Box<dyn std::error::Error>> {
use openpit::{
HasAccountId, HasInstrument, HasOrderPrice, HasTradeAmount, RequestFieldAccessError,
RequestFields,
};
#[derive(RequestFields)]
#[allow(dead_code)]
struct WithMyOperation<T> {
inner: T,
#[openpit(
HasInstrument(instrument -> Result<&Instrument, RequestFieldAccessError>),
HasAccountId(account_id -> Result<AccountId, RequestFieldAccessError>),
HasTradeAmount(trade_amount -> Result<TradeAmount, RequestFieldAccessError>),
HasOrderPrice(price -> Result<Option<Price>, RequestFieldAccessError>)
)]
operation: openpit::OrderOperation,
}
let order = WithMyOperation {
inner: (),
operation: aapl_usd_order("10", "25"),
};
let instrument = order.instrument()?;
assert_eq!(instrument.underlying_asset(), &Asset::new("AAPL")?);
assert_eq!(order.account_id()?, AccountId::from_u64(99224416));
assert_eq!(
order.trade_amount()?,
TradeAmount::Quantity(Quantity::from_str("10")?)
);
assert_eq!(order.price()?, Some(Price::from_str("25")?));
Ok(())
}
#[cfg(feature = "derive")]
#[test]
fn example_wiki_custom_types_inner_field() -> Result<(), Box<dyn std::error::Error>> {
use openpit::{HasInstrument, RequestFieldAccessError, RequestFields};
struct Base {
instrument: Instrument,
}
impl HasInstrument for Base {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
#[derive(RequestFields)]
#[allow(dead_code)]
struct WithMyOperation<T> {
#[openpit(inner, HasInstrument(instrument -> Result<&Instrument, RequestFieldAccessError>))]
base: T,
}
let order = WithMyOperation {
base: Base {
instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
},
};
let instrument = order.instrument()?;
assert_eq!(instrument.underlying_asset(), &Asset::new("AAPL")?);
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, OrderSizeLimit, OrderSizeLimitPolicy, OrderSizeLimitSettings,
};
use openpit::storage::NoLocking;
let engine = Engine::builder::<OrderOperation, PitExecutionReport, ()>()
.no_sync()
.pre_trade(OrderSizeLimitPolicy::<NoLocking>::new(
OrderSizeLimitSettings::new(
None,
[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(())
}
#[cfg(feature = "derive")]
#[test]
fn example_wiki_custom_types_account_adjustment_wrapper() -> Result<(), Box<dyn std::error::Error>>
{
use openpit::param::{AdjustmentAmount, PositionSize};
use openpit::{
HasAccountAdjustmentBalance, HasAccountAdjustmentHeld, HasAccountAdjustmentIncoming,
HasBalanceAsset, RequestFieldAccessError, RequestFields,
};
struct BalanceContext {
asset: Asset,
}
impl HasBalanceAsset for BalanceContext {
fn balance_asset(&self) -> Result<&Asset, RequestFieldAccessError> {
Ok(&self.asset)
}
}
#[derive(RequestFields)]
#[allow(dead_code)]
struct WithAccountAdjustmentAmount<T> {
#[openpit(inner, HasBalanceAsset(balance_asset -> Result<&Asset, RequestFieldAccessError>))]
inner: T,
#[openpit(
HasAccountAdjustmentBalance(balance -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>),
HasAccountAdjustmentHeld(held -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>),
HasAccountAdjustmentIncoming(incoming -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>)
)]
amount: openpit::AccountAdjustmentAmount,
}
let wrapper = WithAccountAdjustmentAmount {
inner: BalanceContext {
asset: Asset::new("USD")?,
},
amount: openpit::AccountAdjustmentAmount {
balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("100")?)),
held: Some(AdjustmentAmount::Delta(PositionSize::from_str("-20")?)),
incoming: Some(AdjustmentAmount::Delta(PositionSize::from_str("5")?)),
},
};
assert_eq!(wrapper.balance_asset()?, &Asset::new("USD")?);
assert_eq!(
wrapper.balance()?,
Some(AdjustmentAmount::Absolute(PositionSize::from_str("100")?))
);
assert_eq!(
wrapper.held()?,
Some(AdjustmentAmount::Delta(PositionSize::from_str("-20")?))
);
assert_eq!(
wrapper.incoming()?,
Some(AdjustmentAmount::Delta(PositionSize::from_str("5")?))
);
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<()>>,
>;
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<()>>,
>;
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: (),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
realized_pnl: 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<()>>,
>;
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: (),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
realized_pnl: 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_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<()>>,
>;
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: (),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
realized_pnl: 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<()>>,
>;
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: (),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
realized_pnl: 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")?,
}),
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_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(())
}