use crate::orderbook::error::OrderBookError;
use dashmap::DashSet;
use pricelevel::{Id, OrderType, PegReferenceType, Side};
use tracing::trace;
#[derive(Debug, Default)]
pub struct SpecialOrderTracker {
pegged_orders: DashSet<Id>,
trailing_stop_orders: DashSet<Id>,
}
impl SpecialOrderTracker {
pub fn new() -> Self {
Self {
pegged_orders: DashSet::new(),
trailing_stop_orders: DashSet::new(),
}
}
pub fn register_pegged_order(&self, order_id: Id) {
self.pegged_orders.insert(order_id);
trace!("Registered pegged order {} for re-pricing", order_id);
}
pub fn register_trailing_stop(&self, order_id: Id) {
self.trailing_stop_orders.insert(order_id);
trace!("Registered trailing stop order {} for re-pricing", order_id);
}
pub fn unregister_pegged_order(&self, order_id: &Id) {
self.pegged_orders.remove(order_id);
trace!("Unregistered pegged order {} from re-pricing", order_id);
}
pub fn unregister_trailing_stop(&self, order_id: &Id) {
self.trailing_stop_orders.remove(order_id);
trace!(
"Unregistered trailing stop order {} from re-pricing",
order_id
);
}
pub fn pegged_order_count(&self) -> usize {
self.pegged_orders.len()
}
pub fn trailing_stop_count(&self) -> usize {
self.trailing_stop_orders.len()
}
pub fn pegged_order_ids(&self) -> Vec<Id> {
let mut ids: Vec<Id> = self.pegged_orders.iter().map(|r| *r).collect();
ids.sort_by_key(|id| id.to_string());
ids
}
pub fn trailing_stop_ids(&self) -> Vec<Id> {
let mut ids: Vec<Id> = self.trailing_stop_orders.iter().map(|r| *r).collect();
ids.sort_by_key(|id| id.to_string());
ids
}
pub fn clear(&self) {
self.pegged_orders.clear();
self.trailing_stop_orders.clear();
}
}
#[derive(Debug, Clone, Default)]
pub struct RepricingResult {
pub pegged_orders_repriced: usize,
pub trailing_stops_repriced: usize,
pub failed_orders: Vec<(Id, String)>,
}
#[allow(clippy::too_many_arguments)]
pub fn calculate_pegged_price(
reference_type: PegReferenceType,
offset: i64,
side: Side,
best_bid: Option<u128>,
best_ask: Option<u128>,
mid_price: Option<u128>,
last_trade: Option<u128>,
tick_size: Option<u128>,
) -> Option<u128> {
let reference_price = match reference_type {
PegReferenceType::BestBid => best_bid?,
PegReferenceType::BestAsk => best_ask?,
PegReferenceType::MidPrice => mid_price?,
PegReferenceType::LastTrade => last_trade?,
};
let mut new_price = if offset >= 0 {
reference_price.saturating_add(offset as u128)
} else {
reference_price.saturating_sub((-offset) as u128)
};
let raw_target = new_price;
let step = tick_size.filter(|t| *t > 1).unwrap_or(1);
match side {
Side::Buy => {
if let Some(ask) = best_ask {
new_price = new_price.min(ask.saturating_sub(step));
}
}
Side::Sell => {
if let Some(bid) = best_bid {
new_price = new_price.max(bid.saturating_add(step));
}
}
}
if step > 1 {
new_price = match side {
Side::Buy => (new_price / step) * step, Side::Sell => new_price.div_ceil(step).saturating_mul(step), };
}
let min_price = if step > 1 { step } else { 1 };
let priced = new_price.max(min_price);
match side {
Side::Buy => {
if best_ask.is_some_and(|a| priced >= a) {
trace!(
"pegged re-price skipped (Buy): no valid passive tick below best_ask; raw target {raw_target} would cross"
);
return None;
}
}
Side::Sell => {
if best_bid.is_some_and(|b| priced <= b) {
trace!(
"pegged re-price skipped (Sell): no valid passive tick above best_bid; raw target {raw_target} would cross"
);
return None;
}
}
}
if priced != raw_target {
trace!(
"pegged price clamped to passive side ({side:?}): requested reference+offset {raw_target} -> resting at {priced}"
);
}
Some(priced)
}
pub fn calculate_trailing_stop_price(
side: Side,
current_stop_price: u128,
trail_amount: u64,
last_reference_price: u128,
current_market_price: u128,
) -> Option<(u128, u128)> {
let trail = trail_amount as u128;
match side {
Side::Sell => {
if current_market_price > last_reference_price {
let new_stop_price = current_market_price.saturating_sub(trail);
if new_stop_price > current_stop_price {
return Some((new_stop_price, current_market_price));
}
}
}
Side::Buy => {
if current_market_price < last_reference_price {
let new_stop_price = current_market_price.saturating_add(trail);
if new_stop_price < current_stop_price {
return Some((new_stop_price, current_market_price));
}
}
}
}
None
}
pub trait RepricingOperations<T> {
fn reprice_pegged_orders(&self) -> Result<usize, OrderBookError>;
fn reprice_trailing_stops(&self) -> Result<usize, OrderBookError>;
fn reprice_special_orders(&self) -> Result<RepricingResult, OrderBookError>;
fn should_trigger_trailing_stop(
&self,
order: &OrderType<T>,
current_market_price: u128,
) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_pegged_price_best_bid() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
5,
Side::Buy,
Some(100),
Some(105),
Some(102),
Some(101),
None, );
assert_eq!(price, Some(104));
}
#[test]
fn test_calculate_pegged_price_best_ask() {
let price = calculate_pegged_price(
PegReferenceType::BestAsk,
-3,
Side::Sell,
Some(100),
Some(105),
Some(102),
Some(101),
None, );
assert_eq!(price, Some(102)); }
#[test]
fn test_calculate_pegged_price_mid_price() {
let price = calculate_pegged_price(
PegReferenceType::MidPrice,
0,
Side::Buy,
Some(100),
Some(110),
Some(105),
Some(103),
None, );
assert_eq!(price, Some(105)); }
#[test]
fn test_calculate_pegged_price_last_trade() {
let price = calculate_pegged_price(
PegReferenceType::LastTrade,
2,
Side::Buy,
Some(100),
Some(105),
Some(102),
Some(101),
None, );
assert_eq!(price, Some(103)); }
#[test]
fn test_calculate_pegged_price_missing_reference() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
5,
Side::Buy,
None, Some(105),
Some(102),
Some(101),
None, );
assert_eq!(price, None);
}
#[test]
fn test_calculate_pegged_price_negative_offset_floor() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
-200, Side::Buy,
Some(100),
Some(105),
Some(102),
Some(101),
None, );
assert_eq!(price, Some(1)); }
#[test]
fn test_trailing_stop_sell_market_rises() {
let result = calculate_trailing_stop_price(
Side::Sell,
95, 5, 100, 110, );
assert_eq!(result, Some((105, 110))); }
#[test]
fn test_trailing_stop_sell_market_falls() {
let result = calculate_trailing_stop_price(
Side::Sell,
95, 5, 100, 90, );
assert_eq!(result, None); }
#[test]
fn test_trailing_stop_buy_market_falls() {
let result = calculate_trailing_stop_price(
Side::Buy,
105, 5, 100, 90, );
assert_eq!(result, Some((95, 90))); }
#[test]
fn test_trailing_stop_buy_market_rises() {
let result = calculate_trailing_stop_price(
Side::Buy,
105, 5, 100, 110, );
assert_eq!(result, None); }
#[test]
fn test_special_order_tracker() {
let tracker = SpecialOrderTracker::new();
let id1 = Id::from_u64(1);
let id2 = Id::from_u64(2);
let id3 = Id::from_u64(3);
tracker.register_pegged_order(id1);
tracker.register_pegged_order(id2);
tracker.register_trailing_stop(id3);
assert_eq!(tracker.pegged_order_count(), 2);
assert_eq!(tracker.trailing_stop_count(), 1);
tracker.unregister_pegged_order(&id1);
assert_eq!(tracker.pegged_order_count(), 1);
tracker.clear();
assert_eq!(tracker.pegged_order_count(), 0);
assert_eq!(tracker.trailing_stop_count(), 0);
}
fn expected_sorted(ids: &[Id]) -> Vec<Id> {
let mut sorted = ids.to_vec();
sorted.sort_by_key(|id| id.to_string());
sorted
}
#[test]
fn test_pegged_order_ids_deterministic_order_issue_106() {
let id10 = Id::sequential(10);
let id2 = Id::sequential(2);
let id33 = Id::sequential(33);
let id1 = Id::sequential(1);
let tracker = SpecialOrderTracker::new();
tracker.register_pegged_order(id10);
tracker.register_pegged_order(id2);
tracker.register_pegged_order(id33);
tracker.register_pegged_order(id1);
let expected = expected_sorted(&[id10, id2, id33, id1]);
let got = tracker.pegged_order_ids();
assert_eq!(got, expected, "pegged_order_ids must be to_string-sorted");
assert_eq!(tracker.pegged_order_ids(), got);
let tracker2 = SpecialOrderTracker::new();
tracker2.register_pegged_order(id1);
tracker2.register_pegged_order(id33);
tracker2.register_pegged_order(id2);
tracker2.register_pegged_order(id10);
assert_eq!(tracker2.pegged_order_ids(), expected);
}
#[test]
fn test_trailing_stop_ids_deterministic_order_issue_106() {
let id10 = Id::sequential(10);
let id2 = Id::sequential(2);
let id33 = Id::sequential(33);
let id1 = Id::sequential(1);
let tracker = SpecialOrderTracker::new();
tracker.register_trailing_stop(id10);
tracker.register_trailing_stop(id2);
tracker.register_trailing_stop(id33);
tracker.register_trailing_stop(id1);
let expected = expected_sorted(&[id10, id2, id33, id1]);
let got = tracker.trailing_stop_ids();
assert_eq!(got, expected, "trailing_stop_ids must be to_string-sorted");
assert_eq!(tracker.trailing_stop_ids(), got);
let tracker2 = SpecialOrderTracker::new();
tracker2.register_trailing_stop(id1);
tracker2.register_trailing_stop(id33);
tracker2.register_trailing_stop(id2);
tracker2.register_trailing_stop(id10);
assert_eq!(tracker2.trailing_stop_ids(), expected);
}
#[test]
fn test_calculate_pegged_price_buy_clamped_below_best_ask_issue_106() {
let best_bid = Some(100_u128);
let best_ask = Some(105_u128);
let price = calculate_pegged_price(
PegReferenceType::BestBid,
20,
Side::Buy,
best_bid,
best_ask,
Some(102),
Some(101),
None, );
assert_eq!(price, Some(104));
assert!(price < best_ask, "must rest below best ask");
}
#[test]
fn test_calculate_pegged_price_sell_clamped_above_best_bid_issue_106() {
let best_bid = Some(100_u128);
let best_ask = Some(105_u128);
let price = calculate_pegged_price(
PegReferenceType::BestAsk,
-20,
Side::Sell,
best_bid,
best_ask,
Some(102),
Some(101),
None, );
assert_eq!(price, Some(101));
assert!(price > best_bid, "must rest above best bid");
}
#[test]
fn test_calculate_pegged_price_no_clamp_when_passive_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
2,
Side::Buy,
Some(100),
Some(105),
Some(102),
Some(101),
None, );
assert_eq!(price, Some(102));
let price = calculate_pegged_price(
PegReferenceType::BestAsk,
-1,
Side::Sell,
Some(100),
Some(105),
Some(102),
Some(101),
None, );
assert_eq!(price, Some(104));
}
#[test]
fn test_calculate_pegged_price_buy_tick_aware_clamp_snaps_to_ask_step_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
7,
Side::Buy,
Some(100),
Some(105),
Some(102),
Some(101),
Some(5),
);
assert_eq!(price, Some(100));
let p = price.expect("price present");
assert!(p.is_multiple_of(5), "must be tick-aligned");
assert!(p < 105, "must rest below best ask");
}
#[test]
fn test_calculate_pegged_price_buy_tick_aware_snaps_offtick_raw_down_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
-3,
Side::Buy,
Some(100),
Some(105),
Some(102),
Some(101),
Some(5),
);
assert_eq!(price, Some(95));
assert!(price.expect("price present").is_multiple_of(5));
}
#[test]
fn test_calculate_pegged_price_sell_tick_aware_clamp_snaps_to_bid_step_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestAsk,
7,
Side::Sell,
Some(100),
Some(105),
Some(102),
Some(101),
Some(5),
);
assert_eq!(price, Some(115));
let p = price.expect("price present");
assert!(p.is_multiple_of(5), "must be tick-aligned");
assert!(p > 100, "must rest above best bid");
}
#[test]
fn test_calculate_pegged_price_sell_tick_aware_clamp_below_bid_snaps_up_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestAsk,
-35,
Side::Sell,
Some(100),
Some(120),
Some(110),
Some(101),
Some(5),
);
assert_eq!(price, Some(105));
let p = price.expect("price present");
assert!(p.is_multiple_of(5));
assert!(p > 100, "must rest above best bid");
}
#[test]
fn test_calculate_pegged_price_buy_no_passive_tick_returns_none_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
10,
Side::Buy,
Some(3),
Some(5),
Some(4),
None,
Some(5),
);
assert_eq!(price, None);
let price = calculate_pegged_price(
PegReferenceType::BestAsk,
0,
Side::Buy,
None,
Some(1),
None,
None,
None, );
assert_eq!(price, None);
}
#[test]
fn test_calculate_pegged_price_sell_floor_stays_passive_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
-10,
Side::Sell,
Some(1),
None,
None,
None,
None, );
assert_eq!(price, Some(2));
}
#[test]
fn test_calculate_pegged_price_sell_no_passive_tick_returns_none_issue_106() {
let price = calculate_pegged_price(
PegReferenceType::BestBid,
10,
Side::Sell,
Some(u128::MAX),
None,
None,
None,
None, );
assert_eq!(price, None);
}
}