use std::collections::HashMap;
use std::hash::Hash;
use crate::param::{Asset, Pnl};
use crate::pretrade::policy::PolicyName;
use crate::pretrade::{AccountBlock, Reject, RejectCode, RejectScope};
pub(super) fn has_configured_bound(lower_bound: &Option<Pnl>, upper_bound: &Option<Pnl>) -> bool {
lower_bound.is_some() || upper_bound.is_some()
}
pub(super) fn breached_sides(
lower_bound: Option<Pnl>,
upper_bound: Option<Pnl>,
realized: Pnl,
) -> Vec<&'static str> {
let mut sides = Vec::new();
if let Some(lb) = lower_bound {
if realized < lb {
sides.push("lower");
}
}
if let Some(ub) = upper_bound {
if realized > ub {
sides.push("upper");
}
}
sides
}
pub(super) fn outside_bounds(
lower_bound: Option<Pnl>,
upper_bound: Option<Pnl>,
realized: Pnl,
) -> bool {
!breached_sides(lower_bound, upper_bound, realized).is_empty()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn barrier_breach_reject(
policy_name: &'static str,
reason: &'static str,
breached_sides: &[&'static str],
lower_bound: Option<Pnl>,
upper_bound: Option<Pnl>,
realized: Pnl,
asset_label: &'static str,
asset: &Asset,
) -> Reject {
let desc = breached_sides.join(" and ");
Reject::new(
policy_name,
RejectScope::Account,
RejectCode::PnlKillSwitchTriggered,
reason,
format!(
"{desc} bound breached: realized pnl {realized}, \
lower_bound {lower_bound:?}, upper_bound {upper_bound:?}, \
{asset_label} {asset}"
),
)
}
pub(super) fn pnl_breach_account_block(
policy_name: &'static str,
details: impl Into<String>,
) -> AccountBlock {
AccountBlock::new(
policy_name,
RejectCode::PnlKillSwitchTriggered,
"pnl kill switch triggered",
details.into(),
)
}
pub(super) fn pnl_calculation_failed_block<Policy: PolicyName + ?Sized>(
policy: &Policy,
reason: &'static str,
details: String,
) -> AccountBlock {
AccountBlock::new(
policy.policy_name(),
RejectCode::OrderValueCalculationFailed,
reason,
details,
)
}
pub(super) fn set_or_clear<Key, Value>(
map: &mut HashMap<Key, Value>,
key: Key,
value: Option<Value>,
) where
Key: Eq + Hash,
{
if let Some(value) = value {
map.insert(key, value);
} else {
map.remove(&key);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn account_id_is_not_leaked_into_barrier_breach_reject() {
let asset = Asset::new("USD").expect("asset literal must be valid");
let reject = barrier_breach_reject(
"pnl-bounds",
"pnl kill switch triggered",
&["lower"],
Some(Pnl::from_str("-100").expect("pnl literal must be valid")),
Some(Pnl::from_str("50").expect("pnl literal must be valid")),
Pnl::from_str("-101").expect("pnl literal must be valid"),
"settlement asset",
&asset,
);
assert!(
!reject.reason.contains("account"),
"reason: {}",
reject.reason
);
assert!(
!reject.details.contains("account"),
"details: {}",
reject.details
);
assert!(reject.details.contains("realized pnl -101"));
assert!(reject.details.contains("settlement asset USD"));
}
}