mod limit_mode;
pub use limit_mode::SpotFundsLimitMode;
use crate::core::sync_mode::SyncMode;
use crate::core::{
HasAccountAdjustmentBalance, HasAccountAdjustmentBalanceAverageEntryPrice,
HasAccountAdjustmentBalanceLowerBound, HasAccountAdjustmentBalanceUpperBound,
HasAccountAdjustmentHeld, HasAccountAdjustmentHeldLowerBound,
HasAccountAdjustmentHeldUpperBound, HasAccountAdjustmentIncoming,
HasAccountAdjustmentIncomingLowerBound, HasAccountAdjustmentIncomingUpperBound,
HasAccountAdjustmentPnlOperation, HasAccountId, HasBalanceAsset, HasExecutionReportFillFee,
HasExecutionReportIsFinal, HasExecutionReportLastTrade, HasInstrument, HasLeavesQuantity,
HasOrderPrice, HasPreTradeLock, HasSide, HasTradeAmount,
};
use crate::marketdata::MarketDataSync;
use crate::param::{AccountId, Asset, Pnl};
use crate::pretrade::holdings::HoldingsStore;
use crate::pretrade::policy::{PolicyGroupId, PolicyName};
use crate::pretrade::ConfigurablePolicy;
use crate::pretrade::PreTradePolicy;
use crate::pretrade::{
PolicyAccountAdjustmentResult, PolicyConfigurationResult, PolicyPreTradeResult,
PolicyRuntimeConfiguration, PostTradeResult, PreTradeContext, Rejects,
};
use crate::storage::{CreateStorageFor, LockingPolicyFactory, Storage, StorageBuilder};
use crate::{AccountAdjustmentContext, Mutations};
mod adjustment;
mod execution;
mod market_data;
mod market_order_pricer;
mod pnl;
mod pre_trade;
mod rejects;
mod rollback;
mod views;
#[cfg(test)]
mod tests;
pub use market_data::{
SpotFundsConfigError, SpotFundsMarketData, SpotFundsOverride, SpotFundsOverrideTarget,
SpotFundsPricingSource, SpotFundsSettings,
};
pub use pnl::{
SpotFundsPnlBoundsAccountBarrier, SpotFundsPnlBoundsAccountGroupBarrier,
SpotFundsPnlBoundsBarrier,
};
const SPOT_FUNDS_POLICY_NAME: &str = "SpotFundsPolicy";
pub(super) type HoldingsKey = (AccountId, Asset);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct AccountPnlEntry {
pub(super) state: crate::PnlState,
pub(super) assertion_token: Option<u64>,
}
impl AccountPnlEntry {
fn zero() -> Self {
Self {
state: crate::PnlState::Value(Pnl::ZERO),
assertion_token: None,
}
}
}
#[derive(Clone, Copy)]
struct AccountPnlLease {
owner_id: u64,
depth: usize,
#[cfg(debug_assertions)]
owner_thread: std::thread::ThreadId,
}
impl AccountPnlLease {
fn new(owner_id: u64) -> Self {
Self {
owner_id,
depth: 1,
#[cfg(debug_assertions)]
owner_thread: std::thread::current().id(),
}
}
}
pub(crate) type AccountPnlStorage<LockingPolicyFactory> =
<LockingPolicyFactory as crate::storage::LockingPolicyFactory>::Shared<
Storage<
AccountId,
AccountPnlEntry,
<LockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy,
>,
>;
type AccountPnlLeaseStorage<LockingPolicyFactory> =
<LockingPolicyFactory as crate::storage::LockingPolicyFactory>::Shared<
Storage<
AccountId,
Option<AccountPnlLease>,
<LockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy,
>,
>;
pub(super) struct AccountPnlLeaseGuard<StorageFactory>
where
StorageFactory: LockingPolicyFactory,
{
leases: AccountPnlLeaseStorage<StorageFactory>,
account_id: AccountId,
owner_id: u64,
}
impl<StorageFactory> Drop for AccountPnlLeaseGuard<StorageFactory>
where
StorageFactory: LockingPolicyFactory,
{
fn drop(&mut self) {
release_account_pnl_lease::<StorageFactory>(&self.leases, self.account_id, self.owner_id);
}
}
fn release_account_pnl_lease<StorageFactory>(
leases: &AccountPnlLeaseStorage<StorageFactory>,
account_id: AccountId,
owner_id: u64,
) where
StorageFactory: LockingPolicyFactory,
{
leases.with_mut_if_present(&account_id, |lease| {
if let Some(existing) = lease {
if existing.owner_id == owner_id {
if existing.depth == 1 {
*lease = None;
} else {
existing.depth -= 1;
}
}
}
});
}
pub struct SpotFundsPolicy<Sync, MarketDataSyncMode>
where
Sync: SyncMode,
Sync::StorageLockingPolicyFactory: LockingPolicyFactory,
MarketDataSyncMode: MarketDataSync,
{
pub(super) holdings: <<Sync as SyncMode>::StorageLockingPolicyFactory
as crate::storage::LockingPolicyFactory>::Shared<
HoldingsStore<
<<Sync as SyncMode>::StorageLockingPolicyFactory
as crate::storage::LockingPolicyFactory>::Policy,
>,
>,
pub(super) settings: <Sync::StorageLockingPolicyFactory
as LockingPolicyFactory>::Config<SpotFundsSettings>,
pub(super) market_orders: Option<SpotFundsMarketData<MarketDataSyncMode>>,
pub(super) pnl: AccountPnlStorage<Sync::StorageLockingPolicyFactory>,
pnl_leases: AccountPnlLeaseStorage<Sync::StorageLockingPolicyFactory>,
group_id: PolicyGroupId,
}
impl<Sync, MarketDataSyncMode> SpotFundsPolicy<Sync, MarketDataSyncMode>
where
Sync: SyncMode,
Sync::StorageLockingPolicyFactory: LockingPolicyFactory,
MarketDataSyncMode: MarketDataSync,
{
pub const NAME: &'static str = SPOT_FUNDS_POLICY_NAME;
pub fn new(
settings: SpotFundsSettings,
market_orders: Option<SpotFundsMarketData<MarketDataSyncMode>>,
storage_builder: &StorageBuilder<<Sync as SyncMode>::StorageLockingPolicyFactory>,
) -> Self
where
<Sync as SyncMode>::StorageLockingPolicyFactory:
CreateStorageFor<(AccountId, Asset)> + CreateStorageFor<AccountId>,
{
let pnl = storage_builder.create_for_bound_key::<AccountId, AccountPnlEntry>();
let pnl =
<Sync::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::new_shared(
pnl,
);
let pnl_leases =
storage_builder.create_for_bound_key::<AccountId, Option<AccountPnlLease>>();
let pnl_leases =
<Sync::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::new_shared(
pnl_leases,
);
Self {
holdings: <<Sync as SyncMode>::StorageLockingPolicyFactory
as crate::storage::LockingPolicyFactory>::new_shared(
HoldingsStore::new(storage_builder),
),
settings: <Sync::StorageLockingPolicyFactory
as LockingPolicyFactory>::new_config(settings),
market_orders,
pnl,
pnl_leases,
group_id: crate::pretrade::DEFAULT_POLICY_GROUP_ID,
}
}
pub fn pnl_bounds_kill_switch<AccountGroupBarriers, AccountBarriers>(
global_barrier: Option<SpotFundsPnlBoundsBarrier>,
account_group_barriers: AccountGroupBarriers,
account_barriers: AccountBarriers,
market_orders: Option<SpotFundsMarketData<MarketDataSyncMode>>,
storage_builder: &StorageBuilder<<Sync as SyncMode>::StorageLockingPolicyFactory>,
) -> Result<Self, SpotFundsConfigError>
where
AccountGroupBarriers: IntoIterator<Item = SpotFundsPnlBoundsAccountGroupBarrier>,
AccountBarriers: IntoIterator<Item = SpotFundsPnlBoundsAccountBarrier>,
<Sync as SyncMode>::StorageLockingPolicyFactory:
CreateStorageFor<(AccountId, Asset)> + CreateStorageFor<AccountId>,
{
let mut settings = SpotFundsSettings::new(
0,
SpotFundsPricingSource::Mark,
std::iter::empty::<(SpotFundsOverrideTarget, SpotFundsOverride)>(),
)?;
settings.set_global_limit_mode(SpotFundsLimitMode::TrackOnly);
let settings =
settings.with_pnl_barriers(global_barrier, account_group_barriers, account_barriers)?;
Ok(Self::new(settings, market_orders, storage_builder))
}
pub(super) fn account_pnl_state(&self, account_id: AccountId) -> crate::PnlState {
self.pnl
.with(&account_id, |entry| entry.state)
.unwrap_or(crate::PnlState::Value(Pnl::ZERO))
}
fn acquire_account_pnl_lease(
&self,
account_id: AccountId,
owner_id: u64,
) -> AccountPnlLeaseGuard<Sync::StorageLockingPolicyFactory> {
let mut attempts = 0_u32;
loop {
let acquired = self.pnl_leases.with_mut(
account_id,
|| None,
|lease, _| match lease {
Some(existing) if existing.owner_id == owner_id => {
existing.depth += 1;
true
}
Some(existing) => {
#[cfg(debug_assertions)]
debug_assert!(
existing.owner_thread != std::thread::current().id(),
"account PnL lease is held by this very thread under \
another owner: waiting for it can never end"
);
let _ = existing;
false
}
None => {
*lease = Some(AccountPnlLease::new(owner_id));
true
}
},
);
if acquired {
return AccountPnlLeaseGuard {
leases: self.pnl_leases.clone(),
account_id,
owner_id,
};
}
if attempts < 8 {
std::thread::yield_now();
} else {
std::thread::sleep(std::time::Duration::from_micros(50));
}
attempts = attempts.saturating_add(1);
}
}
pub(super) fn acquire_account_pnl_assertion(
&self,
account_id: AccountId,
owner_id: u64,
state: crate::PnlState,
) -> (
AccountPnlEntry,
u64,
AccountPnlLeaseGuard<Sync::StorageLockingPolicyFactory>,
) {
let lease = self.acquire_account_pnl_lease(account_id, owner_id);
let token = crate::core::mutation::next_mutation_owner_id();
let previous = self
.pnl
.with_mut(account_id, AccountPnlEntry::zero, |entry, _| {
let previous = *entry;
*entry = AccountPnlEntry {
state,
assertion_token: Some(token),
};
previous
});
(previous, token, lease)
}
pub(super) fn set_account_pnl_state(
&self,
account_id: AccountId,
state: crate::PnlState,
) -> Option<crate::PnlState> {
let owner_id = crate::core::mutation::next_mutation_owner_id();
let _lease = self.acquire_account_pnl_lease(account_id, owner_id);
self.pnl
.with_mut(account_id, AccountPnlEntry::zero, |entry, is_new| {
let previous = (!is_new).then_some(entry.state);
*entry = AccountPnlEntry {
state,
assertion_token: None,
};
previous
})
}
pub(super) fn group_id(&self) -> PolicyGroupId {
self.group_id
}
pub fn with_policy_group_id(mut self, id: PolicyGroupId) -> Self {
self.group_id = id;
self
}
}
impl<Sync, MarketDataSyncMode> PolicyName for SpotFundsPolicy<Sync, MarketDataSyncMode>
where
Sync: SyncMode,
Sync::StorageLockingPolicyFactory: LockingPolicyFactory,
MarketDataSyncMode: MarketDataSync,
{
fn policy_name(&self) -> &str {
Self::NAME
}
}
impl<Order, ExecutionReport, AccountAdjustment, Sync, MarketDataSyncMode>
PreTradePolicy<Order, ExecutionReport, AccountAdjustment, Sync>
for SpotFundsPolicy<Sync, MarketDataSyncMode>
where
Order: HasInstrument + HasAccountId + HasSide + HasTradeAmount + HasOrderPrice,
ExecutionReport: HasInstrument
+ HasAccountId
+ HasSide
+ HasExecutionReportLastTrade
+ HasExecutionReportFillFee
+ HasLeavesQuantity
+ HasExecutionReportIsFinal
+ HasPreTradeLock,
AccountAdjustment: HasBalanceAsset
+ HasAccountAdjustmentBalance
+ HasAccountAdjustmentBalanceAverageEntryPrice
+ HasAccountAdjustmentBalanceLowerBound
+ HasAccountAdjustmentBalanceUpperBound
+ HasAccountAdjustmentHeld
+ HasAccountAdjustmentHeldLowerBound
+ HasAccountAdjustmentHeldUpperBound
+ HasAccountAdjustmentIncoming
+ HasAccountAdjustmentIncomingLowerBound
+ HasAccountAdjustmentIncomingUpperBound
+ HasAccountAdjustmentPnlOperation,
Sync: SyncMode,
Sync::StorageLockingPolicyFactory: LockingPolicyFactory,
MarketDataSyncMode: MarketDataSync,
<<Sync as SyncMode>::StorageLockingPolicyFactory as crate::storage::LockingPolicyFactory>::Policy: 'static,
{
fn name(&self) -> &str {
Self::NAME
}
fn policy_group_id(&self) -> PolicyGroupId {
self.group_id()
}
#[allow(private_interfaces)]
fn built_in_config_entry(
&self,
) -> Option<crate::core::ConfigEntry<<Sync as SyncMode>::StorageLockingPolicyFactory>> {
Some(crate::core::ConfigEntry::SpotFunds {
settings: crate::pretrade::ConfigurablePolicy::settings_cell(self),
})
}
fn apply_runtime_configuration(
&self,
configuration: PolicyRuntimeConfiguration,
) -> PolicyConfigurationResult {
match configuration {
PolicyRuntimeConfiguration::SetSpotFundsAccountPnl {
account_id,
account_group_id,
state,
} => {
self.set_account_pnl_state(account_id, state);
let account_blocks = self
.pnl_barrier_for(account_id, account_group_id)
.as_ref()
.and_then(|barrier| {
rejects::account_pnl_block_for_state(
account_id,
state,
barrier,
None,
)
})
.into_iter()
.collect();
PolicyConfigurationResult { account_blocks }
}
}
}
fn apply_account_adjustment(
&self,
ctx: &AccountAdjustmentContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
account_id: AccountId,
adjustment: &AccountAdjustment,
mutations: &mut Mutations,
) -> Result<PolicyAccountAdjustmentResult, Rejects> {
self.apply_account_adjustment_impl(
Some(ctx.account_control.clone()),
ctx.account_group(),
account_id,
adjustment,
mutations,
)
}
fn apply_execution_report(
&self,
ctx: &crate::pretrade::PostTradeContext<
<Sync as crate::core::SyncMode>::StorageLockingPolicyFactory,
>,
report: &ExecutionReport,
) -> Option<PostTradeResult> {
self.apply_execution_report_impl(ctx, report)
}
fn perform_pre_trade_check(
&self,
ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
order: &Order,
mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
self.perform_pre_trade_check_impl(ctx.account_control.clone(), ctx, order, mutations)
}
fn perform_pre_trade_check_dry_run(
&self,
ctx: &PreTradeContext<<Sync as SyncMode>::StorageLockingPolicyFactory>,
order: &Order,
_mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
self.perform_pre_trade_check_dry_run_impl(ctx, order)
}
}
impl<Sync, MarketDataSyncMode> ConfigurablePolicy<<Sync as SyncMode>::StorageLockingPolicyFactory>
for SpotFundsPolicy<Sync, MarketDataSyncMode>
where
Sync: SyncMode,
Sync::StorageLockingPolicyFactory: LockingPolicyFactory,
MarketDataSyncMode: MarketDataSync,
{
type Settings = SpotFundsSettings;
fn settings_cell(
&self,
) -> <Sync::StorageLockingPolicyFactory as LockingPolicyFactory>::Config<SpotFundsSettings>
{
self.settings.clone()
}
}
#[cfg(test)]
mod lease_tests {
use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::core::mutation::MutationRollbackResult;
use crate::storage::{LockingPolicyFactory, NoLocking, StorageBuilder};
use crate::{Mutation, Mutations};
use super::{AccountPnlLease, AccountPnlLeaseGuard, AccountPnlLeaseStorage};
fn lease_storage() -> AccountPnlLeaseStorage<NoLocking> {
let builder = StorageBuilder::new(NoLocking);
let storage =
builder.create_for_bound_key::<crate::param::AccountId, Option<AccountPnlLease>>();
<NoLocking as LockingPolicyFactory>::new_shared(storage)
}
fn acquire_test_lease(
leases: &AccountPnlLeaseStorage<NoLocking>,
account_id: crate::param::AccountId,
owner_id: u64,
) -> Option<AccountPnlLeaseGuard<NoLocking>> {
let acquired = leases.with_mut(
account_id,
|| None,
|lease, _| {
if lease.is_none() {
*lease = Some(AccountPnlLease::new(owner_id));
true
} else {
false
}
},
);
acquired.then(|| AccountPnlLeaseGuard {
leases: leases.clone(),
account_id,
owner_id,
})
}
#[test]
fn unresolved_mutations_drop_releases_account_pnl_lease() {
let leases = lease_storage();
let account_id = crate::param::AccountId::from_u64(1);
let guard = acquire_test_lease(&leases, account_id, 7)
.expect("initial account PnL lease must be acquired");
let mut mutations = Mutations::new();
mutations.push(Mutation::new_reporting_with_guard(
|| {},
MutationRollbackResult::default,
guard,
));
drop(mutations);
let subsequent = acquire_test_lease(&leases, account_id, 8)
.expect("dropping unresolved mutations must release the lease");
drop(subsequent);
}
#[test]
fn account_pnl_lease_releases_when_rollback_unwinds() {
let leases = lease_storage();
let account_id = crate::param::AccountId::from_u64(1);
let owner_id = 7;
let guard = acquire_test_lease(&leases, account_id, owner_id)
.expect("account PnL lease must be acquired");
let mut mutations = Mutations::new();
mutations.push(Mutation::new_reporting_with_guard(
|| {},
|| -> MutationRollbackResult { panic!("rollback action panic") },
guard,
));
let unwind = catch_unwind(AssertUnwindSafe(|| {
let _ = mutations.rollback_all();
}));
assert!(unwind.is_err());
assert!(leases
.with(&account_id, Option::is_none)
.expect("lease slot must remain present"));
}
#[test]
fn account_pnl_lease_releases_when_commit_unwinds() {
let leases = lease_storage();
let account_id = crate::param::AccountId::from_u64(1);
let guard =
acquire_test_lease(&leases, account_id, 7).expect("account PnL lease must be acquired");
let mut mutations = Mutations::new();
mutations.push(Mutation::new_reporting_with_guard(
|| panic!("commit action panic"),
MutationRollbackResult::default,
guard,
));
let unwind = catch_unwind(AssertUnwindSafe(|| mutations.commit_all()));
assert!(unwind.is_err());
assert!(leases
.with(&account_id, Option::is_none)
.expect("lease slot must remain present"));
}
}