use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use openpit::param::{AccountId, Asset, Fee, Pnl, Price, Quantity, Side, TradeAmount, Volume};
use openpit::pretrade::policies::OrderValidationPolicy;
use openpit::pretrade::{
PreTradeContext, PreTradePolicy, Reject, RejectCode, RejectScope, Rejects,
};
use openpit::{
AccountAdjustmentContext, AccountAdjustmentPolicy, 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> PreTradePolicy<O, R> for ReserveThenValidatePolicy {
fn name(&self) -> &str {
"ReserveThenValidatePolicy"
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext,
_order: &O,
mutations: &mut Mutations,
) -> Result<(), 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>>::name(self),
RejectScope::Order,
RejectCode::RiskLimitExceeded,
"temporary reservation exceeds limit",
format!("reserved {}, limit: {}", next, self.limit),
)));
}
Ok(())
}
fn apply_execution_report(&self, _report: &R) -> bool {
false
}
}
struct NotionalCapPolicy {
max_abs_notional: Volume,
}
impl<O, R> PreTradePolicy<O, R> for NotionalCapPolicy
where
O: HasTradeAmount + HasOrderPrice,
{
fn name(&self) -> &str {
"NotionalCapPolicy"
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext,
order: &O,
_mutations: &mut Mutations,
) -> Result<(), 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>>::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>>::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>>::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>>::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>>::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>>::name(self),
RejectScope::Order,
RejectCode::RiskLimitExceeded,
"strategy cap exceeded",
format!(
"requested notional {}, max allowed: {}",
requested_notional, self.max_abs_notional
),
)));
}
Ok(())
}
fn apply_execution_report(&self, _report: &R) -> bool {
false
}
}
#[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::<OrderOperation, PitExecutionReport>::builder()
.check_pre_trade_start_policy(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::<OrderOperation, PitExecutionReport>::builder()
.check_pre_trade_start_policy(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::<OrderOperation, PitExecutionReport>::builder()
.check_pre_trade_start_policy(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::<OrderOperation, PitExecutionReport>::builder()
.check_pre_trade_start_policy(OrderValidationPolicy::new())
.build()?;
let report = aapl_usd_report("-50", "3.4");
let result = engine.apply_execution_report(&report);
if result.kill_switch_triggered {
eprintln!("halt new orders until the blocked state is cleared");
}
assert!(!result.kill_switch_triggered);
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 {
total: Some(AdjustmentAmount::Absolute(PositionSize::from_f64(10000.0)?)),
reserved: None,
pending: 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 {
total: Some(AdjustmentAmount::Absolute(PositionSize::from_f64(-3.0)?)),
reserved: None,
pending: None,
},
},
];
let engine = Engine::<(), (), AccountAdjustment>::builder().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>>
{
trait HasAssetDelta {
fn asset_id(&self) -> &str;
fn delta(&self) -> Volume;
}
struct BalanceLimitPolicy {
max_total: Volume,
totals: Rc<RefCell<HashMap<String, Volume>>>,
}
impl BalanceLimitPolicy {
fn new(max_total: Volume) -> Self {
Self {
max_total,
totals: Rc::new(RefCell::new(HashMap::new())),
}
}
}
impl<A: HasAssetDelta> AccountAdjustmentPolicy<A> for BalanceLimitPolicy {
fn name(&self) -> &str {
"BalanceLimitPolicy"
}
fn apply_account_adjustment(
&self,
_ctx: &AccountAdjustmentContext,
_account_id: AccountId,
adjustment: &A,
mutations: &mut Mutations,
) -> Result<(), Rejects> {
let asset_id = adjustment.asset_id().to_owned();
let delta = adjustment.delta();
let prev_total = {
let totals = self.totals.borrow();
totals
.get(&asset_id)
.copied()
.unwrap_or(Volume::from_str("0").unwrap())
};
let new_total = prev_total;
if new_total > self.max_total {
return Err(Rejects::from(Reject::new(
<Self as AccountAdjustmentPolicy<A>>::name(self),
RejectScope::Account,
RejectCode::RiskLimitExceeded,
"cumulative adjustment exceeds limit",
format!("asset {asset_id}: {new_total} > {}", self.max_total),
)));
}
self.totals.borrow_mut().insert(asset_id.clone(), new_total);
let rollback_totals = Rc::clone(&self.totals);
let commit_totals = Rc::clone(&self.totals);
let rollback_asset = asset_id.clone();
let commit_asset = asset_id;
let _ = delta;
mutations.push(Mutation::new(
move || {
let _ = commit_totals;
let _ = commit_asset;
},
move || {
rollback_totals
.borrow_mut()
.insert(rollback_asset, prev_total);
},
));
Ok(())
}
}
struct SimpleAdjustment {
asset: String,
delta: Volume,
}
impl HasAssetDelta for SimpleAdjustment {
fn asset_id(&self) -> &str {
&self.asset
}
fn delta(&self) -> Volume {
self.delta
}
}
let policy = BalanceLimitPolicy::new(Volume::from_str("1000000")?);
let engine = Engine::<(), (), SimpleAdjustment>::builder()
.account_adjustment_policy(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::<OrderOperation, PitExecutionReport>::builder()
.pre_trade_policy(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::<OrderOperation, PitExecutionReport>::builder()
.pre_trade_policy(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(())
}
#[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::{
HasAccountAdjustmentPending, HasAccountAdjustmentReserved, HasAccountAdjustmentTotal,
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(
HasAccountAdjustmentTotal(total -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>),
HasAccountAdjustmentReserved(reserved -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>),
HasAccountAdjustmentPending(pending -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>)
)]
amount: openpit::AccountAdjustmentAmount,
}
let wrapper = WithAccountAdjustmentAmount {
inner: BalanceContext {
asset: Asset::new("USD")?,
},
amount: openpit::AccountAdjustmentAmount {
total: Some(AdjustmentAmount::Absolute(PositionSize::from_str("100")?)),
reserved: Some(AdjustmentAmount::Delta(PositionSize::from_str("-20")?)),
pending: Some(AdjustmentAmount::Delta(PositionSize::from_str("5")?)),
},
};
assert_eq!(wrapper.balance_asset()?, &Asset::new("USD")?);
assert_eq!(
wrapper.total()?,
Some(AdjustmentAmount::Absolute(PositionSize::from_str("100")?))
);
assert_eq!(
wrapper.reserved()?,
Some(AdjustmentAmount::Delta(PositionSize::from_str("-20")?))
);
assert_eq!(
wrapper.pending()?,
Some(AdjustmentAmount::Delta(PositionSize::from_str("5")?))
);
Ok(())
}