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::QuantityOverflow { .. } => Self::InvalidQuantity,
225 OrderBookError::OrderSizeOutOfRange { .. } => Self::OrderSizeOutOfRange,
226 OrderBookError::DuplicateOrderId { .. } => Self::DuplicateOrderId,
227 OrderBookError::MissingUserId { .. } => Self::MissingUserId,
228 OrderBookError::PriceLevelError(_) => Self::Other(0),
229 OrderBookError::OrderNotFound(_) => Self::Other(0),
230 OrderBookError::InvalidOperation { .. } => Self::Other(0),
231 OrderBookError::SerializationError { .. } => Self::Other(0),
232 OrderBookError::DeserializationError { .. } => Self::Other(0),
233 OrderBookError::ChecksumMismatch { .. } => Self::Other(0),
234 #[cfg(feature = "nats")]
235 OrderBookError::NatsPublishError { .. } => Self::Other(0),
236 #[cfg(feature = "nats")]
237 OrderBookError::NatsSerializationError { .. } => Self::Other(0),
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use pricelevel::{Hash32, Id, PriceLevelError, Side};
246
247 fn named_variants() -> [RejectReason; 13] {
250 [
251 RejectReason::KillSwitchActive,
252 RejectReason::RiskMaxOpenOrders,
253 RejectReason::RiskMaxNotional,
254 RejectReason::RiskPriceBand,
255 RejectReason::PostOnlyWouldCross,
256 RejectReason::SelfTradePrevention,
257 RejectReason::InvalidPrice,
258 RejectReason::InvalidQuantity,
259 RejectReason::InvalidPriceLevel,
260 RejectReason::OrderSizeOutOfRange,
261 RejectReason::MissingUserId,
262 RejectReason::DuplicateOrderId,
263 RejectReason::InsufficientLiquidity,
264 ]
265 }
266
267 #[test]
268 fn test_discriminants_are_stable() {
269 assert_eq!(RejectReason::KillSwitchActive.as_u16(), 1);
270 assert_eq!(RejectReason::RiskMaxOpenOrders.as_u16(), 2);
271 assert_eq!(RejectReason::RiskMaxNotional.as_u16(), 3);
272 assert_eq!(RejectReason::RiskPriceBand.as_u16(), 4);
273 assert_eq!(RejectReason::PostOnlyWouldCross.as_u16(), 5);
274 assert_eq!(RejectReason::SelfTradePrevention.as_u16(), 6);
275 assert_eq!(RejectReason::InvalidPrice.as_u16(), 7);
276 assert_eq!(RejectReason::InvalidQuantity.as_u16(), 8);
277 assert_eq!(RejectReason::InvalidPriceLevel.as_u16(), 9);
278 assert_eq!(RejectReason::OrderSizeOutOfRange.as_u16(), 10);
279 assert_eq!(RejectReason::MissingUserId.as_u16(), 11);
280 assert_eq!(RejectReason::DuplicateOrderId.as_u16(), 12);
281 assert_eq!(RejectReason::InsufficientLiquidity.as_u16(), 13);
282 }
283
284 #[test]
285 fn test_other_passthrough() {
286 assert_eq!(RejectReason::Other(0).as_u16(), 0);
287 assert_eq!(RejectReason::Other(7777).as_u16(), 7777);
288 assert_eq!(RejectReason::Other(u16::MAX).as_u16(), u16::MAX);
289 }
290
291 #[test]
292 fn test_display_reads_human_text() {
293 for reason in named_variants() {
296 let text = reason.to_string();
297 assert!(!text.is_empty(), "Display for {reason:?} produced empty");
298 }
299 assert_eq!(
300 RejectReason::KillSwitchActive.to_string(),
301 "kill switch active"
302 );
303 assert_eq!(RejectReason::Other(42).to_string(), "other(42)");
304 }
305
306 #[test]
307 fn test_from_order_book_error_kill_switch_maps_to_kill_switch_active() {
308 let err = OrderBookError::KillSwitchActive;
309 assert_eq!(RejectReason::from(&err), RejectReason::KillSwitchActive);
310 }
311
312 #[test]
313 fn test_from_order_book_error_risk_max_open_maps_to_risk_max_open_orders() {
314 let err = OrderBookError::RiskMaxOpenOrders {
315 account: Hash32::from([1u8; 32]),
316 current: 5,
317 limit: 5,
318 };
319 assert_eq!(RejectReason::from(&err), RejectReason::RiskMaxOpenOrders);
320 }
321
322 #[test]
323 fn test_from_order_book_error_risk_max_notional() {
324 let err = OrderBookError::RiskMaxNotional {
325 account: Hash32::from([1u8; 32]),
326 current: 100,
327 attempted: 50,
328 limit: 100,
329 };
330 assert_eq!(RejectReason::from(&err), RejectReason::RiskMaxNotional);
331 }
332
333 #[test]
334 fn test_from_order_book_error_risk_price_band() {
335 let err = OrderBookError::RiskPriceBand {
336 submitted: 1_000_000,
337 reference: 500_000,
338 deviation_bps: 10_000,
339 limit_bps: 100,
340 };
341 assert_eq!(RejectReason::from(&err), RejectReason::RiskPriceBand);
342 }
343
344 #[test]
345 fn test_from_order_book_error_invalid_price_level_maps_to_invalid_price_level() {
346 let err = OrderBookError::InvalidPriceLevel(42);
347 assert_eq!(RejectReason::from(&err), RejectReason::InvalidPriceLevel);
348 }
349
350 #[test]
351 fn test_from_order_book_error_order_size_out_of_range() {
352 let err = OrderBookError::OrderSizeOutOfRange {
353 quantity: 0,
354 min: Some(1),
355 max: Some(100),
356 };
357 assert_eq!(RejectReason::from(&err), RejectReason::OrderSizeOutOfRange);
358 }
359
360 #[test]
361 fn test_from_order_book_error_missing_user_id() {
362 let err = OrderBookError::MissingUserId {
363 order_id: Id::new_uuid(),
364 };
365 assert_eq!(RejectReason::from(&err), RejectReason::MissingUserId);
366 }
367
368 #[test]
369 fn test_from_order_book_error_duplicate_order_id() {
370 let err = OrderBookError::DuplicateOrderId {
371 order_id: Id::new_uuid(),
372 };
373 assert_eq!(RejectReason::from(&err), RejectReason::DuplicateOrderId);
374 }
375
376 #[test]
377 fn test_from_order_book_error_self_trade_prevented_maps_to_self_trade_prevention() {
378 let err = OrderBookError::SelfTradePrevented {
379 mode: crate::orderbook::stp::STPMode::CancelTaker,
380 taker_order_id: Id::new_uuid(),
381 user_id: Hash32::from([1u8; 32]),
382 };
383 assert_eq!(RejectReason::from(&err), RejectReason::SelfTradePrevention);
384 }
385
386 #[test]
387 fn test_from_order_book_error_price_crossing_maps_to_post_only_would_cross() {
388 let err = OrderBookError::PriceCrossing {
389 price: 100,
390 side: Side::Buy,
391 opposite_price: 99,
392 };
393 assert_eq!(RejectReason::from(&err), RejectReason::PostOnlyWouldCross);
394 }
395
396 #[test]
397 fn test_from_order_book_error_invalid_tick_size_maps_to_invalid_price() {
398 let err = OrderBookError::InvalidTickSize {
399 price: 150,
400 tick_size: 100,
401 };
402 assert_eq!(RejectReason::from(&err), RejectReason::InvalidPrice);
403 }
404
405 #[test]
406 fn test_from_order_book_error_invalid_lot_size_maps_to_invalid_quantity() {
407 let err = OrderBookError::InvalidLotSize {
408 quantity: 75,
409 lot_size: 10,
410 };
411 assert_eq!(RejectReason::from(&err), RejectReason::InvalidQuantity);
412 }
413
414 #[test]
415 fn test_from_order_book_error_insufficient_liquidity() {
416 let err = OrderBookError::InsufficientLiquidity {
417 side: Side::Buy,
418 requested: 100,
419 available: 50,
420 };
421 assert_eq!(
422 RejectReason::from(&err),
423 RejectReason::InsufficientLiquidity
424 );
425 }
426
427 #[test]
428 fn test_from_order_book_error_insufficient_liquidity_notional() {
429 let err = OrderBookError::InsufficientLiquidityNotional {
430 side: Side::Buy,
431 requested: 1_000_000,
432 spent: 0,
433 };
434 assert_eq!(
435 RejectReason::from(&err),
436 RejectReason::InsufficientLiquidity
437 );
438 }
439
440 #[test]
441 fn test_from_order_book_error_serialization_error_maps_to_other_zero() {
442 let err = OrderBookError::SerializationError {
443 message: "oops".to_string(),
444 };
445 assert_eq!(RejectReason::from(&err), RejectReason::Other(0));
446 }
447
448 #[test]
449 fn test_from_order_book_error_internal_state_errors_map_to_other_zero() {
450 let cases = [
451 OrderBookError::OrderNotFound("x".to_string()),
452 OrderBookError::InvalidOperation {
453 message: "nope".to_string(),
454 },
455 OrderBookError::DeserializationError {
456 message: "bad".to_string(),
457 },
458 OrderBookError::ChecksumMismatch {
459 expected: "a".to_string(),
460 actual: "b".to_string(),
461 },
462 OrderBookError::PriceLevelError(PriceLevelError::InvalidFormat),
463 ];
464 for err in cases {
465 assert_eq!(
466 RejectReason::from(&err),
467 RejectReason::Other(0),
468 "{err:?} should map to Other(0)"
469 );
470 }
471 }
472
473 #[test]
474 fn test_serde_json_roundtrip_each_variant() {
475 for reason in named_variants() {
476 let json = serde_json::to_string(&reason).expect("serialize named variant");
477 let decoded: RejectReason =
478 serde_json::from_str(&json).expect("deserialize named variant");
479 assert_eq!(decoded, reason);
480 }
481 let other = RejectReason::Other(42);
482 let json = serde_json::to_string(&other).expect("serialize Other(42)");
483 let decoded: RejectReason = serde_json::from_str(&json).expect("deserialize Other(42)");
484 assert_eq!(decoded, other);
485 }
486
487 #[test]
488 fn test_serde_json_emits_stable_u16_wire_code() {
489 for reason in named_variants() {
493 let json = serde_json::to_string(&reason).expect("serialize named variant");
494 assert_eq!(
495 json,
496 reason.as_u16().to_string(),
497 "JSON wire code drift for {reason:?}"
498 );
499 }
500 let other = RejectReason::Other(7777);
501 let json = serde_json::to_string(&other).expect("serialize Other");
502 assert_eq!(json, "7777");
503 }
504
505 #[test]
506 fn test_serde_json_unknown_code_decodes_to_other() {
507 let decoded: RejectReason = serde_json::from_str("999").expect("deserialize unknown code");
512 assert_eq!(decoded, RejectReason::Other(999));
513
514 let decoded: RejectReason =
516 serde_json::from_str("1234").expect("deserialize reserved-range code");
517 assert_eq!(decoded, RejectReason::Other(1234));
518 }
519
520 #[cfg(feature = "bincode")]
521 #[test]
522 fn test_serde_bincode_roundtrip_each_variant() {
523 let cfg = bincode::config::standard();
524 for reason in named_variants() {
525 let bytes = bincode::serde::encode_to_vec(reason, cfg).expect("encode named variant");
526 let (decoded, n) = bincode::serde::decode_from_slice::<RejectReason, _>(&bytes, cfg)
527 .expect("decode named variant");
528 assert_eq!(decoded, reason);
529 assert_eq!(n, bytes.len(), "bincode should consume entire payload");
530 }
531 let other = RejectReason::Other(42);
532 let bytes = bincode::serde::encode_to_vec(other, cfg).expect("encode Other(42)");
533 let (decoded, n) = bincode::serde::decode_from_slice::<RejectReason, _>(&bytes, cfg)
534 .expect("decode Other(42)");
535 assert_eq!(decoded, other);
536 assert_eq!(n, bytes.len());
537 }
538}