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()
}
pub(super) fn barrier_breach_details(
breached_sides: &[&'static str],
lower_bound: Option<Pnl>,
upper_bound: Option<Pnl>,
realized: Pnl,
asset_label: &'static str,
asset: &Asset,
) -> String {
let desc = breached_sides.join(" and ");
let mut details = format!("{desc} bound breached: realized pnl {realized}");
if let Some(lower) = lower_bound {
details.push_str(&format!(", lower bound {lower}"));
}
if let Some(upper) = upper_bound {
details.push_str(&format!(", upper bound {upper}"));
}
details.push_str(&format!(", {asset_label} {asset}"));
details
}
#[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 {
Reject::new(
policy_name,
RejectScope::Account,
RejectCode::PnlKillSwitchTriggered,
reason,
barrier_breach_details(
breached_sides,
lower_bound,
upper_bound,
realized,
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_barrier_currency_mismatch_account_block(
policy_name: &'static str,
details: impl Into<String>,
) -> AccountBlock {
AccountBlock::new(
policy_name,
RejectCode::PnlKillSwitchTriggered,
"pnl barrier currency mismatch",
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 breach_details_preserve_decimal_values_and_configured_bounds() {
let asset = Asset::new("USD").expect("valid asset");
let lower = Pnl::from_str("-1.25").expect("valid lower bound");
let upper = Pnl::from_str("2.75").expect("valid upper bound");
for (side, low, high, realized, expected) in [
("lower", Some(lower), None, "-1.25001", "lower bound breached: realized pnl -1.25001, lower bound -1.25, currency USD"),
("upper", None, Some(upper), "2.75001", "upper bound breached: realized pnl 2.75001, upper bound 2.75, currency USD"),
("lower", Some(lower), Some(upper), "-1.25001", "lower bound breached: realized pnl -1.25001, lower bound -1.25, upper bound 2.75, currency USD"),
] {
assert_eq!(
barrier_breach_details(
&[side], low, high,
Pnl::from_str(realized).expect("valid realized pnl"),
"currency", &asset,
),
expected,
);
}
}
#[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"));
}
}