use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use super::account_groups::AccountGroupsHandle;
use super::engine::EngineInner;
use super::engine_trait::EngineTrait;
use super::sync_mode::SyncMode;
use crate::param::{AccountId, Asset, Pnl};
use crate::pretrade::policies::{
account_pnl_barrier_change_blocks, account_pnl_membership_change_block, AccountPnlStorage,
ActiveHoldingsMutationStorage, OrderSizeLimitSettings, PnlBoundsKillSwitchSettings,
RateLimitSettings, RealizedPnlStorage, SpotFundsHoldingsStorage, SpotFundsSettings,
};
use crate::pretrade::{
AccountBlockOutcome, AccountBlockOutcomes, PolicyConfigurationResult,
PolicyRuntimeConfiguration, PreTradePolicy,
};
use crate::storage::{ConfigCell, LockingPolicyFactory};
pub(crate) enum ConfigEntry<Factory: LockingPolicyFactory> {
RateLimit(Factory::Config<RateLimitSettings>),
PnlBoundsKillSwitch {
settings: Factory::Config<PnlBoundsKillSwitchSettings>,
realized: RealizedPnlStorage<Factory>,
},
SpotFunds {
settings: Factory::Config<SpotFundsSettings>,
transition: parking_lot::Mutex<()>,
pnl: AccountPnlStorage<Factory>,
holdings: SpotFundsHoldingsStorage<Factory>,
active_holdings_mutations: ActiveHoldingsMutationStorage<Factory>,
},
OrderSizeLimit(Factory::Config<OrderSizeLimitSettings>),
}
fn update_serialized_transition<T, Cell, Error>(
transition: &parking_lot::Mutex<()>,
cell: &Cell,
update: impl FnOnce(&mut T) -> Result<(), Error>,
observe: impl FnOnce(&T, &T),
) -> Result<(), Error>
where
T: Clone + 'static,
Cell: ConfigCell<T>,
{
let _transition = transition.lock();
let previous = cell.with(Clone::clone);
let mut current = previous.clone();
update(&mut current)?;
let published = current.clone();
cell.update(|stored| {
*stored = current;
Ok(())
})?;
observe(&previous, &published);
Ok(())
}
fn active_holdings_accounts<Factory: LockingPolicyFactory>(
active: &ActiveHoldingsMutationStorage<Factory>,
) -> Vec<AccountId> {
active
.keys()
.into_iter()
.filter(|account| active.with(account, |count| *count != 0).unwrap_or(false))
.collect()
}
impl<Factory: LockingPolicyFactory> ConfigEntry<Factory> {
fn settings_type_name(&self) -> &'static str {
match self {
Self::RateLimit(_) => std::any::type_name::<Factory::Config<RateLimitSettings>>(),
Self::PnlBoundsKillSwitch { .. } => {
std::any::type_name::<Factory::Config<PnlBoundsKillSwitchSettings>>()
}
Self::SpotFunds { .. } => std::any::type_name::<Factory::Config<SpotFundsSettings>>(),
Self::OrderSizeLimit(_) => {
std::any::type_name::<Factory::Config<OrderSizeLimitSettings>>()
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConfigureError {
UnknownPolicy {
name: String,
},
PolicyTypeMismatch {
name: String,
expected: &'static str,
found: &'static str,
},
Validation {
name: String,
message: String,
},
NestedConfiguration,
}
impl Display for ConfigureError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownPolicy { name } => {
write!(formatter, "no configurable policy named {name}")
}
Self::PolicyTypeMismatch {
name,
expected,
found,
} => write!(
formatter,
"policy {name} has settings type {found}, not {expected}"
),
Self::Validation { name, message } => {
write!(formatter, "policy {name} rejected the update: {message}")
}
Self::NestedConfiguration => write!(
formatter,
"configuration is not reentrant: cannot configure from within \
another configuration callback on the same thread"
),
}
}
}
impl std::error::Error for ConfigureError {}
pub(crate) struct ConfigRegistry<Factory: LockingPolicyFactory> {
entries: HashMap<String, ConfigEntry<Factory>>,
}
impl<Factory: LockingPolicyFactory> ConfigRegistry<Factory> {
pub(crate) fn from_entries(entries: HashMap<String, ConfigEntry<Factory>>) -> Self {
Self { entries }
}
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self {
entries: HashMap::new(),
}
}
pub(crate) fn account_pnl_membership_change_blocks(
&self,
account: AccountId,
previous_group: Option<crate::param::AccountGroupId>,
current_group: Option<crate::param::AccountGroupId>,
previous_currency: Option<Asset>,
current_currency: Option<Asset>,
) -> Vec<crate::pretrade::AccountBlock> {
self.entries
.values()
.filter_map(|entry| {
let ConfigEntry::SpotFunds { settings, pnl, .. } = entry else {
return None;
};
settings.with(|settings| {
account_pnl_membership_change_block::<Factory>(
pnl,
settings,
account,
previous_group,
current_group,
previous_currency.as_ref(),
current_currency.as_ref(),
)
})
})
.collect()
}
fn entry(&self, name: &str) -> Result<&ConfigEntry<Factory>, ConfigureError> {
self.entries
.get(name)
.ok_or_else(|| ConfigureError::UnknownPolicy {
name: name.to_owned(),
})
}
fn type_mismatch<Settings: Clone + 'static>(
name: &str,
entry: &ConfigEntry<Factory>,
) -> ConfigureError {
ConfigureError::PolicyTypeMismatch {
name: name.to_owned(),
expected: std::any::type_name::<Factory::Config<Settings>>(),
found: entry.settings_type_name(),
}
}
fn validation<Error: std::fmt::Display>(name: &str, error: Error) -> ConfigureError {
ConfigureError::Validation {
name: name.to_owned(),
message: error.to_string(),
}
}
}
thread_local! {
static CONFIGURING: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
}
struct ConfiguringGuard {
identity: usize,
}
impl ConfiguringGuard {
fn enter(identity: usize) -> Result<Self, ConfigureError> {
CONFIGURING.with(|configuring| {
let mut active = configuring.borrow_mut();
if active.contains(&identity) {
return Err(ConfigureError::NestedConfiguration);
}
active.push(identity);
Ok(Self { identity })
})
}
}
impl Drop for ConfiguringGuard {
fn drop(&mut self) {
CONFIGURING.with(|configuring| {
let mut active = configuring.borrow_mut();
if let Some(pos) = active.iter().rposition(|id| *id == self.identity) {
active.swap_remove(pos);
}
});
}
}
type RegistryFactory<Trait> =
<<Trait as EngineTrait>::Sync as SyncMode>::StorageLockingPolicyFactory;
pub struct Configurator<Trait: EngineTrait> {
inner: <Trait::Sync as SyncMode>::Strong<EngineInner<Trait>>,
}
impl<Trait: EngineTrait> Clone for Configurator<Trait> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<Trait: EngineTrait> Configurator<Trait> {
pub(crate) fn from_inner(inner: <Trait::Sync as SyncMode>::Strong<EngineInner<Trait>>) -> Self {
Self { inner }
}
fn registry(&self) -> &ConfigRegistry<RegistryFactory<Trait>> {
&self.inner.config_registry
}
fn enter_configuration(&self) -> Result<ConfiguringGuard, ConfigureError> {
ConfiguringGuard::enter(std::ptr::from_ref(self.registry()) as usize)
}
pub fn rate_limit<Error: std::fmt::Display>(
&self,
name: &str,
f: impl FnOnce(&mut RateLimitSettings) -> Result<(), Error>,
) -> Result<(), ConfigureError> {
match self.registry().entry(name)? {
ConfigEntry::RateLimit(cell) => {
let _guard = self.enter_configuration()?;
cell.update(f).map_err(|error| {
ConfigRegistry::<RegistryFactory<Trait>>::validation(name, error)
})
}
entry => Err(ConfigRegistry::<_>::type_mismatch::<RateLimitSettings>(
name, entry,
)),
}
}
pub fn pnl_bounds_killswitch<Error: std::fmt::Display>(
&self,
name: &str,
f: impl FnOnce(&mut PnlBoundsKillSwitchSettings) -> Result<(), Error>,
) -> Result<(), ConfigureError> {
match self.registry().entry(name)? {
ConfigEntry::PnlBoundsKillSwitch { settings, .. } => {
let _guard = self.enter_configuration()?;
settings.update(f).map_err(|error| {
ConfigRegistry::<RegistryFactory<Trait>>::validation(name, error)
})
}
entry => Err(ConfigRegistry::<_>::type_mismatch::<
PnlBoundsKillSwitchSettings,
>(name, entry)),
}
}
pub fn set_account_pnl(
&self,
name: &str,
account: AccountId,
settlement_asset: Asset,
pnl: Pnl,
) -> Result<(), ConfigureError> {
match self.registry().entry(name)? {
ConfigEntry::PnlBoundsKillSwitch { realized, .. } => {
realized.with_mut(
(account, settlement_asset),
|| Pnl::ZERO,
|entry, _is_new| *entry = pnl,
);
Ok(())
}
entry => Err(ConfigRegistry::<_>::type_mismatch::<
PnlBoundsKillSwitchSettings,
>(name, entry)),
}
}
pub fn spot_funds<Error: std::fmt::Display>(
&self,
name: &str,
f: impl FnOnce(&mut SpotFundsSettings) -> Result<(), Error>,
) -> Result<AccountBlockOutcomes, ConfigureError> {
match self.registry().entry(name)? {
ConfigEntry::SpotFunds {
settings,
transition,
pnl,
holdings,
active_holdings_mutations,
..
} => {
let _guard = self.enter_configuration()?;
let account_groups = AccountGroupsHandle::<
<Trait::Sync as SyncMode>::StorageLockingPolicyFactory,
>::from_inner(
self.inner.account_groups.clone()
);
let mut account_blocks = Vec::new();
update_serialized_transition(transition, settings, f, |previous, current| {
self.inner.account_currencies.with_state_transition(|| {
let blocks = account_pnl_barrier_change_blocks::<RegistryFactory<Trait>>(
pnl,
holdings,
previous,
current,
&account_groups.memberships(),
self.inner
.account_currencies
.known_account_keys()
.into_iter()
.chain(active_holdings_accounts::<RegistryFactory<Trait>>(
active_holdings_mutations,
)),
|account, group| {
self.inner.account_currencies.currency_of(account, group)
},
);
account_blocks.reserve(blocks.len());
for (account, block) in blocks {
if self
.inner
.blocked_accounts
.block_account(account, block.clone())
{
account_blocks.push(AccountBlockOutcome {
account_id: account,
block,
});
}
}
});
})
.map_err(|error| {
ConfigRegistry::<RegistryFactory<Trait>>::validation(name, error)
})?;
Ok(AccountBlockOutcomes { account_blocks })
}
entry => Err(ConfigRegistry::<_>::type_mismatch::<SpotFundsSettings>(
name, entry,
)),
}
}
pub fn set_spot_funds_account_pnl(
&self,
name: &str,
account: AccountId,
state: crate::PnlState,
) -> Result<PolicyConfigurationResult, ConfigureError> {
match self.registry().entry(name)? {
ConfigEntry::SpotFunds { .. } => {
let policy = self
.inner
.pre_trade_policies
.iter()
.find(|policy| policy.name() == name)
.ok_or_else(|| ConfigureError::UnknownPolicy {
name: name.to_owned(),
})?;
self.inner.account_currencies.with_state_writer(|| {
let account_groups = AccountGroupsHandle::<
<Trait::Sync as SyncMode>::StorageLockingPolicyFactory,
>::from_inner(
self.inner.account_groups.clone()
);
let account_group_id = account_groups.group_of(account);
let configuration = PolicyRuntimeConfiguration::SetSpotFundsAccountPnl {
account_id: account,
account_group_id,
account_currency: self
.inner
.account_currencies
.currency_of(account, account_group_id),
state,
};
let result = policy.apply_runtime_configuration(configuration);
for block in &result.account_blocks {
self.inner
.blocked_accounts
.block_account(account, block.clone());
}
Ok(result)
})
}
entry => Err(ConfigRegistry::<_>::type_mismatch::<SpotFundsSettings>(
name, entry,
)),
}
}
pub fn order_size_limit<Error: std::fmt::Display>(
&self,
name: &str,
f: impl FnOnce(&mut OrderSizeLimitSettings) -> Result<(), Error>,
) -> Result<(), ConfigureError> {
match self.registry().entry(name)? {
ConfigEntry::OrderSizeLimit(cell) => {
let _guard = self.enter_configuration()?;
cell.update(f).map_err(|error| {
ConfigRegistry::<RegistryFactory<Trait>>::validation(name, error)
})
}
entry => Err(ConfigRegistry::<_>::type_mismatch::<OrderSizeLimitSettings>(name, entry)),
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::param::{AccountId, Asset, Quantity, Side, TradeAmount};
use crate::pretrade::policies::{
OrderSizeBrokerBarrier, OrderSizeLimit, OrderSizeLimitPolicy, OrderSizeLimitPolicyError,
OrderSizeLimitSettings, RateLimit, RateLimitBrokerBarrier, RateLimitPolicy,
RateLimitPolicyError, RateLimitSettings,
};
use crate::storage::{ConfigCell, FullLocking, StorageBuilder};
use crate::{Engine, FullSyncEngine, Instrument, OrderOperation};
use super::{active_holdings_accounts, update_serialized_transition, ConfigureError};
#[derive(Clone)]
struct BlockingReadConfigCell<T>(Arc<Mutex<T>>);
impl<T: Clone + 'static> ConfigCell<T> for BlockingReadConfigCell<T> {
fn new(value: T) -> Self {
Self(Arc::new(Mutex::new(value)))
}
fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let value = self.0.lock().expect("test config mutex must not poison");
f(&value)
}
fn with_snapshot<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let value = {
let value = self.0.lock().expect("test config mutex must not poison");
value.clone()
};
f(&value)
}
fn update<E>(&self, f: impl FnOnce(&mut T) -> Result<(), E>) -> Result<(), E> {
let mut value = self.0.lock().expect("test config mutex must not poison");
let mut next = value.clone();
f(&mut next)?;
*value = next;
Ok(())
}
}
fn broker_barrier(max_orders: usize) -> RateLimitBrokerBarrier {
RateLimitBrokerBarrier {
limit: RateLimit {
max_orders,
window: Duration::from_secs(60),
},
}
}
fn order_size_broker(max_quantity: &str) -> OrderSizeBrokerBarrier {
OrderSizeBrokerBarrier {
limit: OrderSizeLimit {
max_quantity: Some(
Quantity::from_str(max_quantity).expect("quantity literal must be valid"),
),
max_notional: None,
},
}
}
fn build_engine(max_orders: usize) -> FullSyncEngine<OrderOperation> {
let builder = Engine::builder::<OrderOperation, (), ()>().full_sync();
let settings = RateLimitSettings::new(Some(broker_barrier(max_orders)), [], [], [])
.expect("broker barrier is a valid configuration");
let policy = RateLimitPolicy::<FullLocking>::new(settings, builder.storage_builder());
builder
.pre_trade(policy)
.build()
.expect("engine must build")
}
fn build_engine_with_order_size(max_orders: usize) -> FullSyncEngine<OrderOperation> {
let builder = Engine::builder::<OrderOperation, (), ()>().full_sync();
let rate_settings = RateLimitSettings::new(Some(broker_barrier(max_orders)), [], [], [])
.expect("broker barrier is a valid configuration");
let size_settings = OrderSizeLimitSettings::new(Some(order_size_broker("100")), [], [])
.expect("order-size broker barrier is a valid configuration");
let rate_policy =
RateLimitPolicy::<FullLocking>::new(rate_settings, builder.storage_builder());
let size_policy = OrderSizeLimitPolicy::<FullLocking>::new(size_settings);
builder
.pre_trade(rate_policy)
.pre_trade(size_policy)
.build()
.expect("engine must build")
}
fn order(account: u64) -> OrderOperation {
OrderOperation {
instrument: Instrument::new(
Asset::new("AAPL").expect("asset code must be valid"),
Asset::new("USD").expect("asset code must be valid"),
),
account_id: AccountId::from_u64(account),
side: Side::Buy,
trade_amount: TradeAmount::Quantity(
Quantity::from_str("1").expect("quantity literal must be valid"),
),
price: None,
}
}
#[test]
fn spot_funds_transition_gate_prevents_a_stale_observer_from_overtaking() {
use std::sync::mpsc;
let gate = Arc::new(parking_lot::Mutex::new(()));
let cell = BlockingReadConfigCell::new(0_i32);
let (first_observer_tx, first_observer_rx) = mpsc::sync_channel(1);
let (release_first_tx, release_first_rx) = mpsc::sync_channel(1);
let (second_observer_tx, second_observer_rx) = mpsc::sync_channel(1);
std::thread::scope(|scope| {
let first_gate = Arc::clone(&gate);
let first_cell = cell.clone();
let first_update_cell = cell.clone();
let first_observer_cell = cell.clone();
scope.spawn(move || {
update_serialized_transition(
&first_gate,
&first_cell,
|value| {
assert_eq!(first_update_cell.with(|value| *value), 0);
*value = 1;
Ok::<_, ()>(())
},
|previous, current| {
assert_eq!(first_observer_cell.with(|value| *value), 1);
first_observer_tx
.send((*previous, *current))
.expect("first transition must be observed");
release_first_rx
.recv()
.expect("first observer must be released");
},
)
.expect("first update must succeed");
});
assert_eq!(
first_observer_rx
.recv()
.expect("first observer must publish"),
(0, 1)
);
let second_gate = Arc::clone(&gate);
let second_cell = cell.clone();
scope.spawn(move || {
update_serialized_transition(
&second_gate,
&second_cell,
|value| {
*value = 2;
Ok::<_, ()>(())
},
|previous, current| {
second_observer_tx
.send((*previous, *current))
.expect("second transition must be observed");
},
)
.expect("second update must succeed");
});
assert_eq!(cell.with(|value| *value), 1);
assert!(second_observer_rx
.recv_timeout(Duration::from_millis(50))
.is_err());
release_first_tx
.send(())
.expect("first observer must still be waiting");
assert_eq!(
second_observer_rx
.recv_timeout(Duration::from_secs(1))
.expect("second observer must run last"),
(1, 2)
);
});
assert_eq!(cell.with(|value| *value), 2);
}
#[test]
fn spot_funds_transition_helper_keeps_rejected_update_transactional() {
let gate = parking_lot::Mutex::new(());
let cell = BlockingReadConfigCell::new(7_i32);
let mut observed = false;
let rejected = update_serialized_transition(
&gate,
&cell,
|value| {
*value = 8;
Err("rejected")
},
|_, _| observed = true,
);
assert_eq!(rejected, Err("rejected"));
assert_eq!(cell.with(|value| *value), 7);
assert!(!observed);
update_serialized_transition(
&gate,
&cell,
|value| {
*value = 9;
Ok::<_, ()>(())
},
|previous, current| assert_eq!((*previous, *current), (7, 9)),
)
.expect("gate must remain usable after rejection");
assert_eq!(cell.with(|value| *value), 9);
}
#[test]
fn inactive_holdings_marker_is_not_a_retune_candidate() {
let active = StorageBuilder::new(FullLocking).create_shared::<AccountId, usize>();
let account = AccountId::from_u64(42);
active.with_mut(account, || 0, |_, _| {});
assert!(active_holdings_accounts::<FullLocking>(&active).is_empty());
}
#[test]
fn nested_same_thread_configuration_is_rejected_without_deadlock() {
let engine = build_engine(2);
let name = RateLimitPolicy::<FullLocking>::NAME;
for account in 0..2 {
engine
.execute_pre_trade(order(account))
.expect("order within the limit must pass");
}
let nested = RefCell::new(None);
let outer = engine
.configure()
.rate_limit::<ConfigureError>(name, |settings| {
settings
.set_broker(Some(broker_barrier(9)))
.expect("widening to 9 is a valid private-copy edit");
let inner = engine
.configure()
.rate_limit::<RateLimitPolicyError>(name, |settings| {
settings.set_broker(Some(broker_barrier(100)))
});
let error = inner.expect_err("nested configuration must be rejected");
*nested.borrow_mut() = Some(error.clone());
Err(error)
});
assert_eq!(
nested.into_inner(),
Some(ConfigureError::NestedConfiguration)
);
assert_eq!(
outer,
Err(ConfigureError::Validation {
name: name.to_owned(),
message: ConfigureError::NestedConfiguration.to_string(),
})
);
let rejects = engine
.execute_pre_trade(order(2))
.err()
.expect("limit must still be 2 after the rejected retune");
assert_eq!(rejects[0].reason, "rate limit exceeded: broker barrier");
}
#[test]
fn nested_configuration_of_different_policy_in_same_engine_is_rejected() {
let engine = build_engine_with_order_size(2);
let rate_name = RateLimitPolicy::<FullLocking>::NAME;
let size_name = OrderSizeLimitPolicy::<FullLocking>::NAME;
let nested = RefCell::new(None);
let outer = engine
.configure()
.rate_limit::<ConfigureError>(rate_name, |_settings| {
let inner = engine
.configure()
.order_size_limit::<OrderSizeLimitPolicyError>(size_name, |settings| {
settings.set_broker(Some(order_size_broker("200")))
});
let error = inner.expect_err("nested configuration must be rejected");
*nested.borrow_mut() = Some(error.clone());
Err(error)
});
assert_eq!(
nested.into_inner(),
Some(ConfigureError::NestedConfiguration)
);
assert_eq!(
outer,
Err(ConfigureError::Validation {
name: rate_name.to_owned(),
message: ConfigureError::NestedConfiguration.to_string(),
})
);
}
#[test]
fn nested_configuration_of_independent_engine_is_allowed() {
let engine_a = build_engine(2);
let engine_b = build_engine(2);
let name = RateLimitPolicy::<FullLocking>::NAME;
engine_a
.configure()
.rate_limit::<RateLimitPolicyError>(name, |_settings| {
engine_b
.configure()
.rate_limit(name, |settings| {
settings.set_broker(Some(broker_barrier(9)))
})
.expect("independent engine update must succeed");
Ok(())
})
.expect("independent engine configuration must not be rejected");
for account in 0..3 {
engine_b
.execute_pre_trade(order(account))
.expect("engine B broker limit must have been widened");
}
}
}