use openpit::param::{
AccountId, AdjustmentAmount, Asset, Pnl, PositionSize, Price, Quantity, Side, Trade,
TradeAmount,
};
use openpit::pretrade::policies::{
RateLimit, RateLimitBrokerBarrier, RateLimitPolicy, RateLimitSettings, SpotFundsPolicy,
SpotFundsPricingSource, SpotFundsSettings,
};
use openpit::pretrade::{PreTradeDryRunReport, PreTradeLock, RejectCode, DEFAULT_POLICY_GROUP_ID};
use openpit::{
Engine, FullSync, FullSyncEngine, HasAccountAdjustmentBalance,
HasAccountAdjustmentBalanceAverageEntryPrice, HasAccountAdjustmentBalanceLowerBound,
HasAccountAdjustmentBalanceRealizedPnl, HasAccountAdjustmentBalanceUpperBound,
HasAccountAdjustmentHeld, HasAccountAdjustmentHeldLowerBound,
HasAccountAdjustmentHeldUpperBound, HasAccountAdjustmentIncoming,
HasAccountAdjustmentIncomingLowerBound, HasAccountAdjustmentIncomingUpperBound, HasAccountId,
HasBalanceAsset, HasExecutionReportIsFinal, HasExecutionReportLastTrade, HasInstrument,
HasLeavesQuantity, HasPreTradeLock, HasSide, Instrument, OrderOperation,
RequestFieldAccessError, SpotFundsMarketData,
};
type TestOrder = OrderOperation;
type TestEngine = FullSyncEngine<TestOrder, TestReport, TestAdjustment>;
const ACC: u64 = 99224416;
struct TestReport {
instrument: Instrument,
account_id: AccountId,
side: Side,
last_trade: Option<Trade>,
leaves_quantity: Quantity,
is_final: bool,
lock: PreTradeLock,
}
impl HasInstrument for TestReport {
fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
Ok(&self.instrument)
}
}
impl HasAccountId for TestReport {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Ok(self.account_id)
}
}
impl HasSide for TestReport {
fn side(&self) -> Result<Side, RequestFieldAccessError> {
Ok(self.side)
}
}
impl HasExecutionReportLastTrade for TestReport {
fn last_trade(&self) -> Result<Option<Trade>, RequestFieldAccessError> {
Ok(self.last_trade)
}
}
impl HasLeavesQuantity for TestReport {
fn leaves_quantity(&self) -> Result<Quantity, RequestFieldAccessError> {
Ok(self.leaves_quantity)
}
}
impl HasExecutionReportIsFinal for TestReport {
fn is_final(&self) -> Result<bool, RequestFieldAccessError> {
Ok(self.is_final)
}
}
impl HasPreTradeLock for TestReport {
fn lock(&self) -> Result<PreTradeLock, RequestFieldAccessError> {
Ok(self.lock.clone())
}
}
struct TestAdjustment {
asset: Asset,
balance: Option<AdjustmentAmount>,
balance_average_entry_price: Option<Price>,
balance_realized_pnl: Option<Pnl>,
balance_lower: Option<PositionSize>,
balance_upper: Option<PositionSize>,
held: Option<AdjustmentAmount>,
}
impl HasBalanceAsset for TestAdjustment {
fn balance_asset(&self) -> Result<&Asset, RequestFieldAccessError> {
Ok(&self.asset)
}
}
impl HasAccountAdjustmentBalance for TestAdjustment {
fn balance(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(self.balance)
}
}
impl HasAccountAdjustmentBalanceAverageEntryPrice for TestAdjustment {
fn balance_average_entry_price(&self) -> Result<Option<Price>, RequestFieldAccessError> {
Ok(self.balance_average_entry_price)
}
}
impl HasAccountAdjustmentBalanceRealizedPnl for TestAdjustment {
fn balance_realized_pnl(&self) -> Result<Option<Pnl>, RequestFieldAccessError> {
Ok(self.balance_realized_pnl)
}
}
impl HasAccountAdjustmentBalanceLowerBound for TestAdjustment {
fn balance_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.balance_lower)
}
}
impl HasAccountAdjustmentBalanceUpperBound for TestAdjustment {
fn balance_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(self.balance_upper)
}
}
impl HasAccountAdjustmentHeld for TestAdjustment {
fn held(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(self.held)
}
}
impl HasAccountAdjustmentHeldLowerBound for TestAdjustment {
fn held_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentHeldUpperBound for TestAdjustment {
fn held_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncoming for TestAdjustment {
fn incoming(&self) -> Result<Option<AdjustmentAmount>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncomingLowerBound for TestAdjustment {
fn incoming_lower(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
impl HasAccountAdjustmentIncomingUpperBound for TestAdjustment {
fn incoming_upper(&self) -> Result<Option<PositionSize>, RequestFieldAccessError> {
Ok(None)
}
}
fn asset(s: &str) -> Asset {
Asset::new(s).expect("valid asset")
}
fn instr(under: &str, sett: &str) -> Instrument {
Instrument::new(asset(under), asset(sett))
}
fn ps(s: &str) -> PositionSize {
PositionSize::from_str(s).expect("valid position size")
}
fn px(s: &str) -> Price {
Price::from_str(s).expect("valid price")
}
fn pnl(s: &str) -> Pnl {
Pnl::from_str(s).expect("valid pnl")
}
fn qty(s: &str) -> Quantity {
Quantity::from_str(s).expect("valid quantity")
}
fn account() -> AccountId {
AccountId::from_u64(ACC)
}
fn make_order(
side: Side,
instrument: Instrument,
trade_amount: TradeAmount,
price: Option<Price>,
) -> TestOrder {
OrderOperation {
instrument,
account_id: account(),
side,
trade_amount,
price,
}
}
fn make_report(
instrument: Instrument,
side: Side,
last_trade: Option<Trade>,
leaves: Quantity,
is_final: bool,
order_price: Option<Price>,
) -> TestReport {
let lock = order_price
.map(|p| PreTradeLock::from_entries([(DEFAULT_POLICY_GROUP_ID, p)]))
.unwrap_or_default();
TestReport {
instrument,
account_id: account(),
side,
last_trade,
leaves_quantity: leaves,
is_final,
lock,
}
}
fn balance_adjustment(asset_code: &str, amount: AdjustmentAmount) -> TestAdjustment {
TestAdjustment {
asset: asset(asset_code),
balance: Some(amount),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: None,
}
}
fn held_adj(asset_code: &str, amount: AdjustmentAmount) -> TestAdjustment {
TestAdjustment {
asset: asset(asset_code),
balance: None,
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: None,
held: Some(amount),
}
}
fn adj_with_avg_pnl(
asset_code: &str,
balance: Option<AdjustmentAmount>,
average_entry_price: Option<Price>,
realized_pnl: Option<Pnl>,
) -> TestAdjustment {
TestAdjustment {
asset: asset(asset_code),
balance,
balance_average_entry_price: average_entry_price,
balance_realized_pnl: realized_pnl,
balance_lower: None,
balance_upper: None,
held: None,
}
}
fn bounded_balance_adjustment(
asset_code: &str,
amount: AdjustmentAmount,
upper: PositionSize,
) -> TestAdjustment {
TestAdjustment {
asset: asset(asset_code),
balance: Some(amount),
balance_average_entry_price: None,
balance_realized_pnl: None,
balance_lower: None,
balance_upper: Some(upper),
held: None,
}
}
fn build_engine() -> TestEngine {
let builder = Engine::builder::<TestOrder, TestReport, TestAdjustment>().full_sync();
let settings = SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, std::iter::empty())
.expect("settings must build");
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
settings,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
fn seed(engine: &TestEngine, asset_code: &str, amount: &str) {
let adj = balance_adjustment(asset_code, AdjustmentAmount::Absolute(ps(amount)));
engine
.apply_account_adjustment(account(), &[adj])
.expect("seed must succeed");
}
#[test]
fn buy_limit_full_fill_reduces_settlement_and_credits_underlying() {
let engine = build_engine();
seed(&engine, "USD", "10000");
let aapl_usd = instr("AAPL", "USD");
let mut reservation = engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("10")),
Some(px("200")),
))
.expect("pre-trade must accept");
reservation.commit();
let report = make_report(
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("10"),
}),
qty("0"),
true,
Some(px("200")),
);
let post = engine.apply_execution_report(&report);
assert!(post.account_blocks.is_empty());
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("40")),
Some(px("200")),
))
.is_ok(),
"Buy 40 @ 200 must fit USD available = 8000"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("41")),
Some(px("200")),
))
.is_err(),
"Buy 41 @ 200 must exceed USD available = 8000"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Sell,
aapl_usd.clone(),
TradeAmount::Quantity(qty("10")),
None,
))
.is_ok(),
"Sell 10 AAPL must fit available = 10"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Sell,
aapl_usd,
TradeAmount::Quantity(qty("11")),
None,
))
.is_err(),
"Sell 11 AAPL must exceed available = 10"
);
}
#[test]
fn buy_insufficient_funds_rejects_with_state_unchanged() {
let engine = build_engine();
seed(&engine, "USD", "10000");
let aapl_usd = instr("AAPL", "USD");
let result = engine.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("100")),
Some(px("200")),
));
let Err(rejects) = result else {
panic!("must reject: notional exceeds available")
};
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd,
TradeAmount::Quantity(qty("50")),
Some(px("200")),
))
.is_ok(),
"Buy 50 @ 200 (= 10000 notional) must succeed after rejection: USD available = 10000"
);
}
#[test]
fn cancel_with_leftover_releases_unfilled_held() {
let engine = build_engine();
seed(&engine, "USD", "10000");
let aapl_usd = instr("AAPL", "USD");
let mut reservation = engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("10")),
Some(px("200")),
))
.expect("pre-trade must accept");
reservation.commit();
engine.apply_execution_report(&make_report(
aapl_usd.clone(),
Side::Buy,
Some(Trade {
price: px("200"),
quantity: qty("4"),
}),
qty("6"),
false,
Some(px("200")),
));
engine.apply_execution_report(&make_report(
aapl_usd.clone(),
Side::Buy,
None,
qty("6"),
true,
Some(px("200")),
));
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("46")),
Some(px("200")),
))
.is_ok(),
"USD available must be 9200 after release"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("47")),
Some(px("200")),
))
.is_err(),
"Buy 47 @ 200 must exceed USD available = 9200"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Sell,
aapl_usd.clone(),
TradeAmount::Quantity(qty("4")),
None,
))
.is_ok(),
"AAPL available must be 4 from partial fill"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Sell,
aapl_usd,
TradeAmount::Quantity(qty("5")),
None,
))
.is_err(),
"Sell 5 AAPL must exceed available = 4"
);
}
#[test]
fn limits_adjustment_delta_adds_to_available() {
let engine = build_engine();
seed(&engine, "USD", "10000");
let adj = balance_adjustment("USD", AdjustmentAmount::Delta(ps("5000")));
engine
.apply_account_adjustment(account(), &[adj])
.expect("delta adjustment must succeed");
let aapl_usd = instr("AAPL", "USD");
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("75")),
Some(px("200")),
))
.is_ok(),
"Buy 75 @ 200 must fit USD available = 15000"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd,
TradeAmount::Quantity(qty("76")),
Some(px("200")),
))
.is_err(),
"Buy 76 @ 200 must exceed USD available = 15000"
);
}
#[test]
fn limits_adjustment_absolute_creates_entry() {
let engine = build_engine();
let adj = balance_adjustment("EUR", AdjustmentAmount::Absolute(ps("1000")));
engine
.apply_account_adjustment(account(), &[adj])
.expect("absolute adjustment must succeed");
let aapl_eur = instr("AAPL", "EUR");
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_eur.clone(),
TradeAmount::Quantity(qty("5")),
Some(px("200")),
))
.is_ok(),
"Buy 5 @ 200 EUR must fit newly created EUR balance = 1000"
);
assert!(
engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_eur,
TradeAmount::Quantity(qty("6")),
Some(px("200")),
))
.is_err(),
"Buy 6 @ 200 EUR must exceed EUR available = 1000"
);
}
#[test]
fn negative_held_blocks_buy_despite_positive_available() {
let engine = build_engine();
seed(&engine, "USD", "2000");
let adj = held_adj("USD", AdjustmentAmount::Absolute(ps("-2000")));
engine
.apply_account_adjustment(account(), &[adj])
.expect("held adjustment must be accepted");
let aapl_usd = instr("AAPL", "USD");
let result = engine.execute_pre_trade(make_order(
Side::Buy,
aapl_usd,
TradeAmount::Quantity(qty("1")),
Some(px("1")),
));
let Err(rejects) = result else {
panic!("buy must be rejected when held=-2000 cancels out available=2000")
};
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
}
fn build_rate_limited_engine() -> TestEngine {
let builder = Engine::builder::<TestOrder, TestReport, TestAdjustment>().full_sync();
let rate_limit = RateLimitPolicy::new(
RateLimitSettings::new(
Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 1,
window: std::time::Duration::from_secs(60),
},
}),
[],
[],
[],
)
.expect("rate-limit settings must build"),
builder.storage_builder(),
);
let spot_funds = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, std::iter::empty())
.expect("spot-funds settings must build"),
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
builder
.pre_trade(rate_limit)
.pre_trade(spot_funds)
.build()
.expect("engine must build")
}
fn seed_rate_limited(engine: &TestEngine, asset_code: &str, amount: &str) {
let adj = balance_adjustment(asset_code, AdjustmentAmount::Absolute(ps(amount)));
engine
.apply_account_adjustment(account(), &[adj])
.expect("seed must succeed");
}
#[test]
fn dry_run_does_not_spend_rate_limit_budget_or_reserve_spot_funds() {
let engine = build_rate_limited_engine();
seed_rate_limited(&engine, "USD", "10000");
let aapl_usd = instr("AAPL", "USD");
for _ in 0..5 {
let report: PreTradeDryRunReport = engine.execute_pre_trade_dry_run(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("10")),
Some(px("200")),
));
assert!(report.is_pass(), "dry-run of a valid order must pass");
assert!(report.account_block().is_none());
assert_eq!(report.account_adjustments().len(), 1);
}
let mut reservation = engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("50")),
Some(px("200")),
))
.expect("first real order must pass: dry-runs consumed no budget or funds");
reservation.commit();
let Err(rejects) = engine.execute_pre_trade(make_order(
Side::Buy,
aapl_usd,
TradeAmount::Quantity(qty("1")),
Some(px("200")),
)) else {
panic!("second real order must breach the broker limit of 1");
};
assert_eq!(rejects[0].code, RejectCode::RateLimitExceeded);
}
#[test]
fn dry_run_insufficient_funds_reports_reject_and_leaves_state_for_real_call() {
let engine = build_rate_limited_engine();
seed_rate_limited(&engine, "USD", "1000");
let aapl_usd = instr("AAPL", "USD");
let report = engine.execute_pre_trade_dry_run(make_order(
Side::Buy,
aapl_usd.clone(),
TradeAmount::Quantity(qty("10")),
Some(px("200")),
));
assert!(!report.is_pass());
let rejects = report.rejects().expect("dry-run must report rejects");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
let mut reservation = engine
.execute_pre_trade(make_order(
Side::Buy,
aapl_usd,
TradeAmount::Quantity(qty("5")),
Some(px("200")),
))
.expect("real order within available must pass after a rejecting dry-run");
reservation.commit();
}
#[test]
fn spot_funds_dry_run_same_asset_negative_sell_rejects_like_real_and_leaves_state() {
let usd_usd = instr("USD", "USD");
let dry_run_engine = build_engine();
seed(&dry_run_engine, "USD", "10");
let report = dry_run_engine.execute_pre_trade_dry_run(make_order(
Side::Sell,
usd_usd.clone(),
TradeAmount::Quantity(qty("8")),
Some(px("-1")),
));
assert!(!report.is_pass());
let rejects = report.rejects().expect("dry-run must report rejects");
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
assert!(report.account_adjustments().is_empty());
let mut reservation = dry_run_engine
.execute_pre_trade(make_order(
Side::Sell,
usd_usd.clone(),
TradeAmount::Quantity(qty("5")),
Some(px("-1")),
))
.expect("dry-run must leave the original USD balance available");
reservation.commit();
let real_engine = build_engine();
seed(&real_engine, "USD", "10");
let Err(rejects) = real_engine.execute_pre_trade(make_order(
Side::Sell,
usd_usd.clone(),
TradeAmount::Quantity(qty("8")),
Some(px("-1")),
)) else {
panic!("real pre-trade must reject after the first same-asset hold")
};
assert_eq!(rejects[0].code, RejectCode::InsufficientFunds);
let mut reservation = real_engine
.execute_pre_trade(make_order(
Side::Sell,
usd_usd,
TradeAmount::Quantity(qty("5")),
Some(px("-1")),
))
.expect("real rejection rollback must restore the USD balance");
reservation.commit();
}
#[test]
fn adjustment_outcome_surfaces_average_and_realized_pnl() {
let engine = build_engine();
engine
.apply_account_adjustment(
account(),
&[adj_with_avg_pnl(
"AAPL",
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(px("100")),
Some(pnl("30")),
)],
)
.expect("seed must succeed");
let result = engine
.apply_account_adjustment(
account(),
&[adj_with_avg_pnl(
"AAPL",
Some(AdjustmentAmount::Delta(ps("0"))),
Some(px("150")),
Some(pnl("50")),
)],
)
.expect("force-set must succeed");
let entry = &result
.outcomes
.first()
.expect("one outcome entry expected")
.entry;
assert_eq!(entry.asset, asset("AAPL"));
assert_eq!(entry.average_entry_price, Some(px("150")));
let pnl_outcome = entry
.realized_pnl
.expect("realized PnL must be surfaced on a force-set");
assert_eq!(pnl_outcome.delta, pnl("20"));
assert_eq!(pnl_outcome.absolute, pnl("50"));
}
#[test]
fn adjustment_outcome_realized_pnl_zero_boundary_is_tracked() {
let engine = build_engine();
let result = engine
.apply_account_adjustment(
account(),
&[adj_with_avg_pnl(
"AAPL",
Some(AdjustmentAmount::Absolute(ps("10"))),
None,
Some(pnl("0")),
)],
)
.expect("force-set must succeed");
let entry = &result
.outcomes
.first()
.expect("one outcome entry expected")
.entry;
let pnl_outcome = entry
.realized_pnl
.expect("a zero force-set still surfaces a tracked realized PnL");
assert_eq!(pnl_outcome.delta, pnl("0"));
assert_eq!(pnl_outcome.absolute, pnl("0"));
}
#[test]
fn rejected_batch_rolls_untracked_realized_pnl_back_to_none() {
let engine = build_engine();
engine
.apply_account_adjustment(
account(),
&[
adj_with_avg_pnl(
"AAPL",
Some(AdjustmentAmount::Absolute(ps("10"))),
Some(px("100")),
None,
),
balance_adjustment("USD", AdjustmentAmount::Absolute(ps("1000"))),
],
)
.expect("seed must succeed");
let err = engine
.apply_account_adjustment(
account(),
&[
adj_with_avg_pnl(
"AAPL",
Some(AdjustmentAmount::Delta(ps("0"))),
None,
Some(pnl("25")),
),
bounded_balance_adjustment("USD", AdjustmentAmount::Delta(ps("5000")), ps("2000")),
],
)
.expect_err("second element must breach its upper bound");
assert_eq!(err.failed_adjustment_index, 1);
let aapl_usd = instr("AAPL", "USD");
let mut reservation = engine
.execute_pre_trade(make_order(
Side::Sell,
aapl_usd.clone(),
TradeAmount::Quantity(qty("4")),
Some(px("130")),
))
.expect("sell pre-trade must accept");
reservation.commit();
let post = engine.apply_execution_report(&make_report(
aapl_usd,
Side::Sell,
Some(Trade {
price: px("130"),
quantity: qty("4"),
}),
qty("0"),
true,
Some(px("130")),
));
let aapl_entry = post
.account_adjustments
.iter()
.find(|o| o.entry.asset == asset("AAPL"))
.expect("AAPL post-trade entry must exist");
assert!(
aapl_entry.entry.realized_pnl.is_none(),
"untracked slot must not auto-resume realized-PnL tracking after rollback"
);
}