1use crate::orderbook::error::OrderBookError;
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58#[non_exhaustive]
59#[repr(u16)]
60pub enum RejectReason {
61 KillSwitchActive = 1,
63 RiskMaxOpenOrders = 2,
65 RiskMaxNotional = 3,
67 RiskPriceBand = 4,
70 PostOnlyWouldCross = 5,
73 SelfTradePrevention = 6,
75 InvalidPrice = 7,
77 InvalidQuantity = 8,
79 InvalidPriceLevel = 9,
81 OrderSizeOutOfRange = 10,
83 MissingUserId = 11,
85 DuplicateOrderId = 12,
87 InsufficientLiquidity = 13,
90 Other(u16),
94}
95
96impl RejectReason {
97 #[inline]
103 #[must_use]
104 pub fn as_u16(self) -> u16 {
105 match self {
106 Self::KillSwitchActive => 1,
107 Self::RiskMaxOpenOrders => 2,
108 Self::RiskMaxNotional => 3,
109 Self::RiskPriceBand => 4,
110 Self::PostOnlyWouldCross => 5,
111 Self::SelfTradePrevention => 6,
112 Self::InvalidPrice => 7,
113 Self::InvalidQuantity => 8,
114 Self::InvalidPriceLevel => 9,
115 Self::OrderSizeOutOfRange => 10,
116 Self::MissingUserId => 11,
117 Self::DuplicateOrderId => 12,
118 Self::InsufficientLiquidity => 13,
119 Self::Other(code) => code,
120 }
121 }
122
123 #[inline]
128 #[must_use]
129 pub fn from_u16(code: u16) -> Self {
130 match code {
131 1 => Self::KillSwitchActive,
132 2 => Self::RiskMaxOpenOrders,
133 3 => Self::RiskMaxNotional,
134 4 => Self::RiskPriceBand,
135 5 => Self::PostOnlyWouldCross,
136 6 => Self::SelfTradePrevention,
137 7 => Self::InvalidPrice,
138 8 => Self::InvalidQuantity,
139 9 => Self::InvalidPriceLevel,
140 10 => Self::OrderSizeOutOfRange,
141 11 => Self::MissingUserId,
142 12 => Self::DuplicateOrderId,
143 13 => Self::InsufficientLiquidity,
144 other => Self::Other(other),
145 }
146 }
147}
148
149impl Serialize for RejectReason {
156 #[inline]
157 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
158 serializer.serialize_u16(self.as_u16())
159 }
160}
161
162impl<'de> Deserialize<'de> for RejectReason {
169 #[inline]
170 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
171 let code = u16::deserialize(deserializer)?;
172 Ok(Self::from_u16(code))
173 }
174}
175
176impl std::fmt::Display for RejectReason {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 match self {
179 Self::KillSwitchActive => write!(f, "kill switch active"),
180 Self::RiskMaxOpenOrders => write!(f, "risk: max open orders"),
181 Self::RiskMaxNotional => write!(f, "risk: max notional"),
182 Self::RiskPriceBand => write!(f, "risk: price band"),
183 Self::PostOnlyWouldCross => write!(f, "post-only would cross"),
184 Self::SelfTradePrevention => write!(f, "self-trade prevention"),
185 Self::InvalidPrice => write!(f, "invalid price"),
186 Self::InvalidQuantity => write!(f, "invalid quantity"),
187 Self::InvalidPriceLevel => write!(f, "invalid price level"),
188 Self::OrderSizeOutOfRange => write!(f, "order size out of range"),
189 Self::MissingUserId => write!(f, "missing user id"),
190 Self::DuplicateOrderId => write!(f, "duplicate order id"),
191 Self::InsufficientLiquidity => write!(f, "insufficient liquidity"),
192 Self::Other(code) => write!(f, "other({code})"),
193 }
194 }
195}
196
197impl From<&OrderBookError> for RejectReason {
210 #[inline]
211 fn from(err: &OrderBookError) -> Self {
212 match err {
213 OrderBookError::KillSwitchActive => Self::KillSwitchActive,
214 OrderBookError::RiskMaxOpenOrders { .. } => Self::RiskMaxOpenOrders,
215 OrderBookError::RiskMaxNotional { .. } => Self::RiskMaxNotional,
216 OrderBookError::RiskPriceBand { .. } => Self::RiskPriceBand,
217 OrderBookError::SelfTradePrevented { .. } => Self::SelfTradePrevention,
218 OrderBookError::InvalidPriceLevel(_) => Self::InvalidPriceLevel,
219 OrderBookError::PriceCrossing { .. } => Self::PostOnlyWouldCross,
220 OrderBookError::InsufficientLiquidity { .. } => Self::InsufficientLiquidity,
221 OrderBookError::InsufficientLiquidityNotional { .. } => Self::InsufficientLiquidity,
222 OrderBookError::InvalidTickSize { .. } => Self::InvalidPrice,
223 OrderBookError::InvalidLotSize { .. } => Self::InvalidQuantity,
224 OrderBookError::OrderSizeOutOfRange { .. } => Self::OrderSizeOutOfRange,
225 OrderBookError::DuplicateOrderId { .. } => Self::DuplicateOrderId,
226 OrderBookError::MissingUserId { .. } => Self::MissingUserId,
227 OrderBookError::PriceLevelError(_) => Self::Other(0),
228 OrderBookError::OrderNotFound(_) => Self::Other(0),
229 OrderBookError::InvalidOperation { .. } => Self::Other(0),
230 OrderBookError::SerializationError { .. } => Self::Other(0),
231 OrderBookError::DeserializationError { .. } => Self::Other(0),
232 OrderBookError::ChecksumMismatch { .. } => Self::Other(0),
233 #[cfg(feature = "nats")]
234 OrderBookError::NatsPublishError { .. } => Self::Other(0),
235 #[cfg(feature = "nats")]
236 OrderBookError::NatsSerializationError { .. } => Self::Other(0),
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use pricelevel::{Hash32, Id, PriceLevelError, Side};
245
246 fn named_variants() -> [RejectReason; 13] {
249 [
250 RejectReason::KillSwitchActive,
251 RejectReason::RiskMaxOpenOrders,
252 RejectReason::RiskMaxNotional,
253 RejectReason::RiskPriceBand,
254 RejectReason::PostOnlyWouldCross,
255 RejectReason::SelfTradePrevention,
256 RejectReason::InvalidPrice,
257 RejectReason::InvalidQuantity,
258 RejectReason::InvalidPriceLevel,
259 RejectReason::OrderSizeOutOfRange,
260 RejectReason::MissingUserId,
261 RejectReason::DuplicateOrderId,
262 RejectReason::InsufficientLiquidity,
263 ]
264 }
265
266 #[test]
267 fn test_discriminants_are_stable() {
268 assert_eq!(RejectReason::KillSwitchActive.as_u16(), 1);
269 assert_eq!(RejectReason::RiskMaxOpenOrders.as_u16(), 2);
270 assert_eq!(RejectReason::RiskMaxNotional.as_u16(), 3);
271 assert_eq!(RejectReason::RiskPriceBand.as_u16(), 4);
272 assert_eq!(RejectReason::PostOnlyWouldCross.as_u16(), 5);
273 assert_eq!(RejectReason::SelfTradePrevention.as_u16(), 6);
274 assert_eq!(RejectReason::InvalidPrice.as_u16(), 7);
275 assert_eq!(RejectReason::InvalidQuantity.as_u16(), 8);
276 assert_eq!(RejectReason::InvalidPriceLevel.as_u16(), 9);
277 assert_eq!(RejectReason::OrderSizeOutOfRange.as_u16(), 10);
278 assert_eq!(RejectReason::MissingUserId.as_u16(), 11);
279 assert_eq!(RejectReason::DuplicateOrderId.as_u16(), 12);
280 assert_eq!(RejectReason::InsufficientLiquidity.as_u16(), 13);
281 }
282
283 #[test]
284 fn test_other_passthrough() {
285 assert_eq!(RejectReason::Other(0).as_u16(), 0);
286 assert_eq!(RejectReason::Other(7777).as_u16(), 7777);
287 assert_eq!(RejectReason::Other(u16::MAX).as_u16(), u16::MAX);
288 }
289
290 #[test]
291 fn test_display_reads_human_text() {
292 for reason in named_variants() {
295 let text = reason.to_string();
296 assert!(!text.is_empty(), "Display for {reason:?} produced empty");
297 }
298 assert_eq!(
299 RejectReason::KillSwitchActive.to_string(),
300 "kill switch active"
301 );
302 assert_eq!(RejectReason::Other(42).to_string(), "other(42)");
303 }
304
305 #[test]
306 fn test_from_order_book_error_kill_switch_maps_to_kill_switch_active() {
307 let err = OrderBookError::KillSwitchActive;
308 assert_eq!(RejectReason::from(&err), RejectReason::KillSwitchActive);
309 }
310
311 #[test]
312 fn test_from_order_book_error_risk_max_open_maps_to_risk_max_open_orders() {
313 let err = OrderBookError::RiskMaxOpenOrders {
314 account: Hash32::from([1u8; 32]),
315 current: 5,
316 limit: 5,
317 };
318 assert_eq!(RejectReason::from(&err), RejectReason::RiskMaxOpenOrders);
319 }
320
321 #[test]
322 fn test_from_order_book_error_risk_max_notional() {
323 let err = OrderBookError::RiskMaxNotional {
324 account: Hash32::from([1u8; 32]),
325 current: 100,
326 attempted: 50,
327 limit: 100,
328 };
329 assert_eq!(RejectReason::from(&err), RejectReason::RiskMaxNotional);
330 }
331
332 #[test]
333 fn test_from_order_book_error_risk_price_band() {
334 let err = OrderBookError::RiskPriceBand {
335 submitted: 1_000_000,
336 reference: 500_000,
337 deviation_bps: 10_000,
338 limit_bps: 100,
339 };
340 assert_eq!(RejectReason::from(&err), RejectReason::RiskPriceBand);
341 }
342
343 #[test]
344 fn test_from_order_book_error_invalid_price_level_maps_to_invalid_price_level() {
345 let err = OrderBookError::InvalidPriceLevel(42);
346 assert_eq!(RejectReason::from(&err), RejectReason::InvalidPriceLevel);
347 }
348
349 #[test]
350 fn test_from_order_book_error_order_size_out_of_range() {
351 let err = OrderBookError::OrderSizeOutOfRange {
352 quantity: 0,
353 min: Some(1),
354 max: Some(100),
355 };
356 assert_eq!(RejectReason::from(&err), RejectReason::OrderSizeOutOfRange);
357 }
358
359 #[test]
360 fn test_from_order_book_error_missing_user_id() {
361 let err = OrderBookError::MissingUserId {
362 order_id: Id::new_uuid(),
363 };
364 assert_eq!(RejectReason::from(&err), RejectReason::MissingUserId);
365 }
366
367 #[test]
368 fn test_from_order_book_error_duplicate_order_id() {
369 let err = OrderBookError::DuplicateOrderId {
370 order_id: Id::new_uuid(),
371 };
372 assert_eq!(RejectReason::from(&err), RejectReason::DuplicateOrderId);
373 }
374
375 #[test]
376 fn test_from_order_book_error_self_trade_prevented_maps_to_self_trade_prevention() {
377 let err = OrderBookError::SelfTradePrevented {
378 mode: crate::orderbook::stp::STPMode::CancelTaker,
379 taker_order_id: Id::new_uuid(),
380 user_id: Hash32::from([1u8; 32]),
381 };
382 assert_eq!(RejectReason::from(&err), RejectReason::SelfTradePrevention);
383 }
384
385 #[test]
386 fn test_from_order_book_error_price_crossing_maps_to_post_only_would_cross() {
387 let err = OrderBookError::PriceCrossing {
388 price: 100,
389 side: Side::Buy,
390 opposite_price: 99,
391 };
392 assert_eq!(RejectReason::from(&err), RejectReason::PostOnlyWouldCross);
393 }
394
395 #[test]
396 fn test_from_order_book_error_invalid_tick_size_maps_to_invalid_price() {
397 let err = OrderBookError::InvalidTickSize {
398 price: 150,
399 tick_size: 100,
400 };
401 assert_eq!(RejectReason::from(&err), RejectReason::InvalidPrice);
402 }
403
404 #[test]
405 fn test_from_order_book_error_invalid_lot_size_maps_to_invalid_quantity() {
406 let err = OrderBookError::InvalidLotSize {
407 quantity: 75,
408 lot_size: 10,
409 };
410 assert_eq!(RejectReason::from(&err), RejectReason::InvalidQuantity);
411 }
412
413 #[test]
414 fn test_from_order_book_error_insufficient_liquidity() {
415 let err = OrderBookError::InsufficientLiquidity {
416 side: Side::Buy,
417 requested: 100,
418 available: 50,
419 };
420 assert_eq!(
421 RejectReason::from(&err),
422 RejectReason::InsufficientLiquidity
423 );
424 }
425
426 #[test]
427 fn test_from_order_book_error_insufficient_liquidity_notional() {
428 let err = OrderBookError::InsufficientLiquidityNotional {
429 side: Side::Buy,
430 requested: 1_000_000,
431 spent: 0,
432 };
433 assert_eq!(
434 RejectReason::from(&err),
435 RejectReason::InsufficientLiquidity
436 );
437 }
438
439 #[test]
440 fn test_from_order_book_error_serialization_error_maps_to_other_zero() {
441 let err = OrderBookError::SerializationError {
442 message: "oops".to_string(),
443 };
444 assert_eq!(RejectReason::from(&err), RejectReason::Other(0));
445 }
446
447 #[test]
448 fn test_from_order_book_error_internal_state_errors_map_to_other_zero() {
449 let cases = [
450 OrderBookError::OrderNotFound("x".to_string()),
451 OrderBookError::InvalidOperation {
452 message: "nope".to_string(),
453 },
454 OrderBookError::DeserializationError {
455 message: "bad".to_string(),
456 },
457 OrderBookError::ChecksumMismatch {
458 expected: "a".to_string(),
459 actual: "b".to_string(),
460 },
461 OrderBookError::PriceLevelError(PriceLevelError::InvalidFormat),
462 ];
463 for err in cases {
464 assert_eq!(
465 RejectReason::from(&err),
466 RejectReason::Other(0),
467 "{err:?} should map to Other(0)"
468 );
469 }
470 }
471
472 #[test]
473 fn test_serde_json_roundtrip_each_variant() {
474 for reason in named_variants() {
475 let json = serde_json::to_string(&reason).expect("serialize named variant");
476 let decoded: RejectReason =
477 serde_json::from_str(&json).expect("deserialize named variant");
478 assert_eq!(decoded, reason);
479 }
480 let other = RejectReason::Other(42);
481 let json = serde_json::to_string(&other).expect("serialize Other(42)");
482 let decoded: RejectReason = serde_json::from_str(&json).expect("deserialize Other(42)");
483 assert_eq!(decoded, other);
484 }
485
486 #[test]
487 fn test_serde_json_emits_stable_u16_wire_code() {
488 for reason in named_variants() {
492 let json = serde_json::to_string(&reason).expect("serialize named variant");
493 assert_eq!(
494 json,
495 reason.as_u16().to_string(),
496 "JSON wire code drift for {reason:?}"
497 );
498 }
499 let other = RejectReason::Other(7777);
500 let json = serde_json::to_string(&other).expect("serialize Other");
501 assert_eq!(json, "7777");
502 }
503
504 #[test]
505 fn test_serde_json_unknown_code_decodes_to_other() {
506 let decoded: RejectReason = serde_json::from_str("999").expect("deserialize unknown code");
511 assert_eq!(decoded, RejectReason::Other(999));
512
513 let decoded: RejectReason =
515 serde_json::from_str("1234").expect("deserialize reserved-range code");
516 assert_eq!(decoded, RejectReason::Other(1234));
517 }
518
519 #[cfg(feature = "bincode")]
520 #[test]
521 fn test_serde_bincode_roundtrip_each_variant() {
522 let cfg = bincode::config::standard();
523 for reason in named_variants() {
524 let bytes = bincode::serde::encode_to_vec(reason, cfg).expect("encode named variant");
525 let (decoded, n) = bincode::serde::decode_from_slice::<RejectReason, _>(&bytes, cfg)
526 .expect("decode named variant");
527 assert_eq!(decoded, reason);
528 assert_eq!(n, bytes.len(), "bincode should consume entire payload");
529 }
530 let other = RejectReason::Other(42);
531 let bytes = bincode::serde::encode_to_vec(other, cfg).expect("encode Other(42)");
532 let (decoded, n) = bincode::serde::decode_from_slice::<RejectReason, _>(&bytes, cfg)
533 .expect("decode Other(42)");
534 assert_eq!(decoded, other);
535 assert_eq!(n, bytes.len());
536 }
537}