1use std::sync::atomic::{AtomicU64, Ordering};
31
32use nautilus_core::{UnixNanos, string::secret::SecretString, time::get_atomic_clock_realtime};
33use nautilus_model::{
34 enums::{OrderSide, OrderType, TimeInForce},
35 events::OrderDeniedReason,
36 identifiers::VenueOrderId,
37 orders::{Order, OrderAny},
38 types::Price,
39};
40use rust_decimal::Decimal;
41use ustr::Ustr;
42
43use crate::{
44 common::{
45 consts::{LOT_SIZE_SCALE, POLYMARKET_NAUTILUS_BUILDER_CODE, USDC_DECIMALS},
46 enums::{PolymarketOrderSide, PolymarketOrderType, PolymarketSignatureType},
47 },
48 http::models::PolymarketOrder,
49 signing::eip712::{OrderSigner, order_hash},
50};
51
52pub const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
54
55const GTD_EXPIRATION_BUFFER_SECS: u64 = 180;
57
58#[derive(Debug)]
64pub struct PolymarketOrderBuilder {
65 order_signer: OrderSigner,
66 signer_address: String,
67 maker_address: String,
68 signature_type: PolymarketSignatureType,
69 last_timestamp_ms: AtomicU64,
70}
71
72impl PolymarketOrderBuilder {
73 pub fn new(
75 order_signer: OrderSigner,
76 signer_address: String,
77 maker_address: String,
78 signature_type: PolymarketSignatureType,
79 ) -> Self {
80 Self {
81 order_signer,
82 signer_address,
83 maker_address,
84 signature_type,
85 last_timestamp_ms: AtomicU64::new(0),
86 }
87 }
88
89 fn next_timestamp_ms(&self) -> u64 {
92 let now_ms = get_atomic_clock_realtime().get_time_ns().as_u64() / 1_000_000;
93
94 loop {
95 let prev = self.last_timestamp_ms.load(Ordering::Relaxed);
96 let candidate = prev.saturating_add(1).max(now_ms);
97
98 if self
99 .last_timestamp_ms
100 .compare_exchange_weak(prev, candidate, Ordering::Relaxed, Ordering::Relaxed)
101 .is_ok()
102 {
103 return candidate;
104 }
105 }
106 }
107
108 #[expect(clippy::too_many_arguments)]
117 pub fn build_limit_order(
118 &self,
119 token_id: &str,
120 side: PolymarketOrderSide,
121 price: Decimal,
122 quantity: Decimal,
123 order_type: PolymarketOrderType,
124 expiration: &str,
125 neg_risk: bool,
126 tick_decimals: u32,
127 ) -> anyhow::Result<PolymarketOrder> {
128 validate_immediate_buy_maker_amount(price, quantity, side, order_type)
129 .map_err(anyhow::Error::msg)?;
130 let (maker_amount, taker_amount) =
131 compute_maker_taker_amounts(price, quantity, side, tick_decimals);
132 self.build_and_sign(
133 token_id,
134 side,
135 maker_amount,
136 taker_amount,
137 expiration,
138 neg_risk,
139 )
140 }
141
142 pub(crate) fn build_limit_order_from_collateral(
148 &self,
149 token_id: &str,
150 price: Decimal,
151 amount: Decimal,
152 expiration: &str,
153 neg_risk: bool,
154 tick_decimals: u32,
155 ) -> anyhow::Result<PolymarketOrder> {
156 anyhow::ensure!(
157 price > Decimal::ZERO,
158 "Polymarket collateral-sized limit BUY price must be positive"
159 );
160
161 let (maker_amount, taker_amount) =
162 compute_quote_buy_maker_taker_amounts(price, amount, tick_decimals);
163 anyhow::ensure!(
164 maker_amount > Decimal::ZERO,
165 "Polymarket collateral-sized limit BUY amount {} pUSD truncates to zero at {LOT_SIZE_SCALE} decimal places",
166 amount.normalize(),
167 );
168 anyhow::ensure!(
169 taker_amount > Decimal::ZERO,
170 "Polymarket collateral-sized limit BUY derives a zero share quantity"
171 );
172 anyhow::ensure!(
173 taker_amount * price == maker_amount,
174 "Polymarket collateral-sized limit BUY amount {} pUSD cannot preserve limit price {} after venue quantization",
175 amount.normalize(),
176 price.normalize(),
177 );
178
179 self.build_and_sign(
180 token_id,
181 PolymarketOrderSide::Buy,
182 maker_amount,
183 taker_amount,
184 expiration,
185 neg_risk,
186 )
187 }
188
189 pub fn build_market_order(
197 &self,
198 token_id: &str,
199 side: PolymarketOrderSide,
200 price: Decimal,
201 amount: Decimal,
202 neg_risk: bool,
203 tick_decimals: u32,
204 ) -> anyhow::Result<PolymarketOrder> {
205 if side == PolymarketOrderSide::Buy && amount.trunc_with_scale(LOT_SIZE_SCALE).is_zero() {
206 anyhow::bail!(
207 "Polymarket market BUY amount {} pUSD truncates to zero at {LOT_SIZE_SCALE} decimal places",
208 amount.normalize(),
209 );
210 }
211
212 let (maker_amount, taker_amount) =
213 compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
214 self.build_and_sign(token_id, side, maker_amount, taker_amount, "0", neg_risk)
215 }
216
217 pub fn expected_order_id(
219 &self,
220 order: &PolymarketOrder,
221 neg_risk: bool,
222 ) -> anyhow::Result<VenueOrderId> {
223 let hash = order_hash(order, neg_risk)
224 .map_err(|e| anyhow::anyhow!("Failed to derive order hash: {e}"))?;
225 let order_id = format!("{hash:#x}");
226 Ok(VenueOrderId::from(order_id.as_str()))
227 }
228
229 pub fn validate_limit_order(order: &OrderAny) -> Result<(), OrderDeniedReason> {
231 if order.is_reduce_only() {
232 return Err(validation_failed(
233 "Reduce-only orders not supported on Polymarket",
234 ));
235 }
236
237 if order.order_type() != OrderType::Limit {
238 return Err(OrderDeniedReason::UnsupportedOrderType {
239 order_type: order.order_type(),
240 });
241 }
242
243 if order.is_quote_quantity() && order.order_side() == OrderSide::Sell {
244 return Err(validation_failed(
245 "Limit SELL orders require quote_quantity=false (amount in shares)",
246 ));
247 }
248
249 let quantity = signed_limit_order_quantity(order.quantity().as_decimal());
250 if quantity.is_zero() {
251 let denomination = if order.is_quote_quantity() {
252 "pUSD"
253 } else {
254 "shares"
255 };
256 return Err(validation_failed(format!(
257 "Polymarket limit order amount {} {denomination} truncates to zero at {LOT_SIZE_SCALE} decimal places",
258 order.quantity(),
259 )));
260 }
261
262 let price = order
263 .price()
264 .ok_or_else(|| validation_failed("Limit orders require a price"))?;
265
266 let order_type = PolymarketOrderType::try_from(order.time_in_force())
267 .map_err(|_| OrderDeniedReason::UnsupportedTimeInForce(order.time_in_force()))?;
268
269 let side = PolymarketOrderSide::from(order.order_side());
270
271 if order.is_post_only()
272 && !matches!(order.time_in_force(), TimeInForce::Gtc | TimeInForce::Gtd)
273 {
274 return Err(validation_failed(
275 "Post-only orders require GTC or GTD time in force",
276 ));
277 }
278
279 if !order.is_quote_quantity() {
280 validate_immediate_buy_maker_amount(
281 price.as_decimal(),
282 order.quantity().as_decimal(),
283 side,
284 order_type,
285 )
286 .map_err(validation_failed)?;
287 }
288
289 Ok(())
290 }
291
292 pub(crate) fn validate_limit_price(
293 order: &OrderAny,
294 tick_size: Price,
295 ) -> Result<(), OrderDeniedReason> {
296 let price = order
297 .price()
298 .ok_or_else(|| validation_failed("Limit orders require a price"))?;
299 Self::validate_limit_price_value(price, tick_size)
300 }
301
302 pub(crate) fn validate_limit_price_value(
303 price: Price,
304 tick_size: Price,
305 ) -> Result<(), OrderDeniedReason> {
306 let tick = tick_size.as_decimal();
307 let price_decimal = price.as_decimal();
308
309 validate_price(price_decimal, tick, "Limit order price", &price.to_string())
310 .map_err(validation_failed)
311 }
312
313 pub(crate) fn validate_limit_expiration(
314 order: &OrderAny,
315 ts_now: UnixNanos,
316 ) -> Result<(), OrderDeniedReason> {
317 if order.time_in_force() != TimeInForce::Gtd {
318 return Ok(());
319 }
320
321 let expire_time = order
322 .expire_time()
323 .filter(|expire_time| !expire_time.is_zero())
324 .ok_or(OrderDeniedReason::MissingExpireTime)?;
325 let now_secs = ts_now.as_seconds();
326 let expire_secs = expire_time.as_seconds();
327 let minimum_expire_secs = now_secs.saturating_add(GTD_EXPIRATION_BUFFER_SECS);
328
329 if expire_secs < minimum_expire_secs {
330 return Err(validation_failed(format!(
331 "Polymarket GTD expiry must be at least {GTD_EXPIRATION_BUFFER_SECS} seconds in the future"
332 )));
333 }
334
335 Ok(())
336 }
337
338 pub fn validate_market_order(order: &OrderAny) -> Result<(), OrderDeniedReason> {
340 if order.is_reduce_only() {
341 return Err(validation_failed(
342 "Reduce-only orders not supported on Polymarket",
343 ));
344 }
345
346 if order.order_type() != OrderType::Market {
347 return Err(OrderDeniedReason::UnsupportedOrderType {
348 order_type: order.order_type(),
349 });
350 }
351
352 match order.order_side() {
355 OrderSide::Buy => {
356 if !order.is_quote_quantity() {
357 return Err(validation_failed(
358 "Market BUY orders require quote_quantity=true (amount in pUSD)",
359 ));
360 }
361 }
362 OrderSide::Sell => {
363 if order.is_quote_quantity() {
364 return Err(validation_failed(
365 "Market SELL orders require quote_quantity=false (amount in shares)",
366 ));
367 }
368 }
369 }
370
371 if PolymarketOrderType::from_market_time_in_force(order.time_in_force()).is_err() {
372 return Err(OrderDeniedReason::UnsupportedTimeInForce(
373 order.time_in_force(),
374 ));
375 }
376
377 Ok(())
378 }
379
380 pub(crate) fn normalize_market_price(
381 price: Decimal,
382 tick_size: Price,
383 tick_decimals: u32,
384 ) -> Result<Decimal, String> {
385 let tick = tick_size.as_decimal();
386 let price = price.trunc_with_scale(tick_decimals);
387
388 validate_price(
389 price,
390 tick,
391 "Derived market price",
392 &price.normalize().to_string(),
393 )?;
394
395 Ok(price)
396 }
397
398 fn build_and_sign(
399 &self,
400 token_id: &str,
401 side: PolymarketOrderSide,
402 maker_amount: Decimal,
403 taker_amount: Decimal,
404 expiration: &str,
405 neg_risk: bool,
406 ) -> anyhow::Result<PolymarketOrder> {
407 let salt = generate_salt();
408 let timestamp_ms = self.next_timestamp_ms();
409
410 let mut poly_order = PolymarketOrder {
411 salt,
412 maker: self.maker_address.clone(),
413 signer: self.order_signer_address(),
414 token_id: Ustr::from(token_id),
415 maker_amount,
416 taker_amount,
417 side,
418 signature_type: self.signature_type,
419 expiration: expiration.to_string(),
420 timestamp: timestamp_ms.to_string(),
421 metadata: ZERO_BYTES32.to_string(),
422 builder: POLYMARKET_NAUTILUS_BUILDER_CODE.to_string(),
423 signature: SecretString::default(),
424 };
425
426 let signature = self
427 .order_signer
428 .sign_order(&poly_order, neg_risk)
429 .map_err(|e| anyhow::anyhow!("EIP-712 signing failed: {e}"))?;
430 poly_order.signature = SecretString::from(signature);
431
432 Ok(poly_order)
433 }
434
435 fn order_signer_address(&self) -> String {
436 match self.signature_type {
437 PolymarketSignatureType::Poly1271 => self.maker_address.clone(),
438 _ => self.signer_address.clone(),
439 }
440 }
441}
442
443fn validation_failed(detail: impl Into<String>) -> OrderDeniedReason {
444 OrderDeniedReason::ValidationFailed {
445 detail: detail.into(),
446 }
447}
448
449fn validate_price(
450 price: Decimal,
451 tick: Decimal,
452 label: &str,
453 price_display: &str,
454) -> Result<(), String> {
455 let tick = tick.normalize();
456 let max_price = Decimal::ONE - tick;
457
458 if price < tick || price > max_price {
459 return Err(format!(
460 "{label} {price_display} outside Polymarket range [{tick}, {max_price}]"
461 ));
462 }
463
464 if price % tick != Decimal::ZERO {
465 return Err(format!(
466 "{label} {price_display} does not conform to Polymarket tick size {tick}"
467 ));
468 }
469
470 Ok(())
471}
472
473fn to_fixed_decimal(d: Decimal) -> Decimal {
474 let mantissa = d.normalize().trunc_with_scale(USDC_DECIMALS).mantissa();
475 Decimal::from(mantissa)
476}
477
478pub(crate) fn signed_limit_order_quantity(quantity: Decimal) -> Decimal {
480 quantity.trunc_with_scale(LOT_SIZE_SCALE)
481}
482
483fn validate_immediate_buy_maker_amount(
484 price: Decimal,
485 quantity: Decimal,
486 side: PolymarketOrderSide,
487 order_type: PolymarketOrderType,
488) -> Result<(), String> {
489 if side != PolymarketOrderSide::Buy
490 || !matches!(
491 order_type,
492 PolymarketOrderType::FAK | PolymarketOrderType::FOK
493 )
494 {
495 return Ok(());
496 }
497
498 let quantity = signed_limit_order_quantity(quantity);
499 let maker_amount = (quantity * price).normalize();
500 if maker_amount.scale() > LOT_SIZE_SCALE {
501 return Err(format!(
502 "Polymarket {order_type} BUY maker amount {} pUSD exceeds 2 decimal places for price {} and quantity {}",
503 maker_amount,
504 price.normalize(),
505 quantity.normalize(),
506 ));
507 }
508
509 Ok(())
510}
511
512pub fn compute_maker_taker_amounts(
521 price: Decimal,
522 quantity: Decimal,
523 side: PolymarketOrderSide,
524 tick_decimals: u32,
525) -> (Decimal, Decimal) {
526 let precision = tick_decimals + LOT_SIZE_SCALE;
527 let qty = signed_limit_order_quantity(quantity);
528
529 match side {
530 PolymarketOrderSide::Buy => {
531 let maker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
532 let taker_amount = to_fixed_decimal(qty);
533 (maker_amount, taker_amount)
534 }
535 PolymarketOrderSide::Sell => {
536 let maker_amount = to_fixed_decimal(qty);
537 let taker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
538 (maker_amount, taker_amount)
539 }
540 }
541}
542
543pub fn compute_market_maker_taker_amounts(
553 price: Decimal,
554 amount: Decimal,
555 side: PolymarketOrderSide,
556 tick_decimals: u32,
557) -> (Decimal, Decimal) {
558 let precision = tick_decimals + LOT_SIZE_SCALE;
559 let amt = amount.trunc_with_scale(LOT_SIZE_SCALE);
560
561 match side {
562 PolymarketOrderSide::Buy => {
563 compute_quote_buy_maker_taker_amounts(price, amount, tick_decimals)
564 }
565 PolymarketOrderSide::Sell => {
566 let maker_amount = to_fixed_decimal(amt);
567 let taker_amount = to_fixed_decimal((amt * price).trunc_with_scale(precision));
568 (maker_amount, taker_amount)
569 }
570 }
571}
572
573fn compute_quote_buy_maker_taker_amounts(
574 price: Decimal,
575 amount: Decimal,
576 tick_decimals: u32,
577) -> (Decimal, Decimal) {
578 let precision = tick_decimals + LOT_SIZE_SCALE;
579 let maker_amount = amount.trunc_with_scale(LOT_SIZE_SCALE);
580 let taker_amount = (maker_amount / price).trunc_with_scale(precision);
581 (
582 to_fixed_decimal(maker_amount),
583 to_fixed_decimal(taker_amount),
584 )
585}
586
587pub fn generate_salt() -> u64 {
593 let bytes = uuid::Uuid::new_v4().into_bytes();
594 u64::from_le_bytes(bytes[..8].try_into().unwrap()) & ((1u64 << 53) - 1)
595}
596
597#[cfg(test)]
598mod tests {
599 use nautilus_core::UUID4;
600 use nautilus_model::{
601 enums::{OrderSide, OrderType, TimeInForce},
602 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
603 orders::{LimitOrder, MarketOrder, OrderAny},
604 types::{Price, Quantity},
605 };
606 use rstest::rstest;
607 use rust_decimal::Decimal;
608 use rust_decimal_macros::dec;
609
610 use super::*;
611 use crate::{
612 common::{credential::EvmPrivateKey, enums::PolymarketOrderSide},
613 execution::parse::adjust_market_buy_amount,
614 signing::eip712::OrderSigner,
615 };
616
617 fn make_limit(
618 reduce_only: bool,
619 quote_quantity: bool,
620 post_only: bool,
621 tif: TimeInForce,
622 ) -> OrderAny {
623 make_limit_at_price(
624 reduce_only,
625 quote_quantity,
626 post_only,
627 tif,
628 Price::from("0.50"),
629 )
630 }
631
632 fn make_limit_at_price(
633 reduce_only: bool,
634 quote_quantity: bool,
635 post_only: bool,
636 tif: TimeInForce,
637 price: Price,
638 ) -> OrderAny {
639 make_limit_with_quantity(
640 OrderSide::Buy,
641 Quantity::from("10"),
642 reduce_only,
643 quote_quantity,
644 post_only,
645 tif,
646 price,
647 )
648 }
649
650 fn make_limit_with_quantity(
651 side: OrderSide,
652 quantity: Quantity,
653 reduce_only: bool,
654 quote_quantity: bool,
655 post_only: bool,
656 tif: TimeInForce,
657 price: Price,
658 ) -> OrderAny {
659 let expire_time = if tif == TimeInForce::Gtd {
660 Some(UnixNanos::from(2_000_000_000_000_000_000u64))
661 } else {
662 None
663 };
664 OrderAny::Limit(LimitOrder::new(
665 TraderId::from("TESTER-001"),
666 StrategyId::from("S-001"),
667 InstrumentId::from("TEST.POLYMARKET"),
668 ClientOrderId::from("O-001"),
669 side,
670 quantity,
671 price,
672 tif,
673 expire_time,
674 post_only,
675 reduce_only,
676 quote_quantity,
677 None,
678 None,
679 None,
680 None,
681 None,
682 None,
683 None,
684 None,
685 None,
686 None,
687 None,
688 UUID4::new(),
689 UnixNanos::default(),
690 ))
691 }
692
693 fn with_expire_time(mut order: OrderAny, expire_time: Option<UnixNanos>) -> OrderAny {
694 let OrderAny::Limit(limit) = &mut order else {
695 unreachable!("test order is limit")
696 };
697 limit.expire_time = expire_time;
698 order
699 }
700
701 fn make_market(side: OrderSide, quote_quantity: bool) -> OrderAny {
702 OrderAny::Market(MarketOrder::new(
703 TraderId::from("TESTER-001"),
704 StrategyId::from("S-001"),
705 InstrumentId::from("TEST.POLYMARKET"),
706 ClientOrderId::from("O-001"),
707 side,
708 Quantity::from("10"),
709 TimeInForce::Ioc,
710 UUID4::new(),
711 UnixNanos::default(),
712 false,
713 quote_quantity,
714 None,
715 None,
716 None,
717 None,
718 None,
719 None,
720 None,
721 None,
722 ))
723 }
724
725 #[rstest]
726 fn test_validate_limit_order_valid() {
727 let order = make_limit(false, false, false, TimeInForce::Gtc);
728 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
729 }
730
731 #[rstest]
732 #[case("0.000", "0.001", "0.001", "0.999")]
733 #[case("1.000", "0.001", "0.001", "0.999")]
734 #[case("0.0000", "0.0001", "0.0001", "0.9999")]
735 #[case("1.0000", "0.0001", "0.0001", "0.9999")]
736 fn test_validate_limit_order_price_out_of_range_denied(
737 #[case] price: &str,
738 #[case] tick_size: &str,
739 #[case] min_price: &str,
740 #[case] max_price: &str,
741 ) {
742 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
743 let err = PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size))
744 .unwrap_err();
745 assert_eq!(
746 err,
747 OrderDeniedReason::ValidationFailed {
748 detail: format!(
749 "Limit order price {price} outside Polymarket range [{min_price}, {max_price}]"
750 ),
751 }
752 );
753 }
754
755 #[rstest]
756 #[case("0.001", "0.001")]
757 #[case("0.999", "0.001")]
758 #[case("0.0001", "0.0001")]
759 #[case("0.9999", "0.0001")]
760 #[case("0.005", "0.005")]
761 #[case("0.995", "0.005")]
762 #[case("0.0025", "0.0025")]
763 #[case("0.9975", "0.0025")]
764 fn test_validate_limit_order_price_boundary_allowed(
765 #[case] price: &str,
766 #[case] tick_size: &str,
767 ) {
768 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
769 assert!(
770 PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size)).is_ok()
771 );
772 }
773
774 #[rstest]
775 #[case("0.505", "0.005")]
776 #[case("0.5025", "0.0025")]
777 fn test_validate_limit_order_price_aligned_allowed(
778 #[case] price: &str,
779 #[case] tick_size: &str,
780 ) {
781 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
782 assert!(
783 PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size)).is_ok()
784 );
785 }
786
787 #[rstest]
788 #[case("0.501", "0.005")]
789 #[case("0.501", "0.0025")]
790 fn test_validate_limit_order_price_misaligned_denied(
791 #[case] price: &str,
792 #[case] tick_size: &str,
793 ) {
794 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
795 let error = PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size))
796 .unwrap_err();
797 assert_eq!(
798 error,
799 OrderDeniedReason::ValidationFailed {
800 detail: format!(
801 "Limit order price {price} does not conform to Polymarket tick size {tick_size}"
802 ),
803 }
804 );
805 }
806
807 #[rstest]
808 #[case::half_cent("0.5059", "0.005", 3, dec!(0.505))]
809 #[case::half_cent_p4("0.5059", "0.0050", 3, dec!(0.505))]
810 #[case::quarter_cent("0.50259", "0.0025", 4, dec!(0.5025))]
811 fn test_normalize_market_price(
812 #[case] price: &str,
813 #[case] tick_size: &str,
814 #[case] tick_decimals: u32,
815 #[case] expected: Decimal,
816 ) {
817 let normalized = PolymarketOrderBuilder::normalize_market_price(
818 price.parse::<Decimal>().unwrap(),
819 Price::from(tick_size),
820 tick_decimals,
821 )
822 .unwrap();
823
824 assert_eq!(normalized, expected);
825 }
826
827 #[rstest]
828 #[case::below_range(
829 "0.0049",
830 "0.005",
831 "Derived market price 0.004 outside Polymarket range [0.005, 0.995]"
832 )]
833 #[case::above_range(
834 "1.0000",
835 "0.0025",
836 "Derived market price 1 outside Polymarket range [0.0025, 0.9975]"
837 )]
838 #[case::off_half_cent(
839 "0.5019",
840 "0.005",
841 "Derived market price 0.501 does not conform to Polymarket tick size 0.005"
842 )]
843 #[case::off_quarter_cent(
844 "0.50129",
845 "0.0025",
846 "Derived market price 0.5012 does not conform to Polymarket tick size 0.0025"
847 )]
848 fn test_normalize_market_price_denied(
849 #[case] price: &str,
850 #[case] tick_size: &str,
851 #[case] expected: &str,
852 ) {
853 let tick_size = Price::from(tick_size);
854 let error = PolymarketOrderBuilder::normalize_market_price(
855 price.parse().unwrap(),
856 tick_size,
857 u32::from(tick_size.precision),
858 )
859 .unwrap_err();
860
861 assert_eq!(error, expected);
862 }
863
864 #[rstest]
865 #[case::below(
866 1_700_000_179_999_999_999,
867 Some("Polymarket GTD expiry must be at least 180 seconds in the future")
868 )]
869 #[case::equal(1_700_000_180_000_000_000, None)]
870 #[case::above(1_700_000_181_000_000_000, None)]
871 fn test_validate_limit_expiration_boundary(
872 #[case] expire_ns: u64,
873 #[case] expected_error: Option<&str>,
874 ) {
875 let order = with_expire_time(
876 make_limit(false, false, false, TimeInForce::Gtd),
877 Some(UnixNanos::from(expire_ns)),
878 );
879 let ts_now = UnixNanos::from(1_700_000_000_900_000_000u64);
880
881 let result = PolymarketOrderBuilder::validate_limit_expiration(&order, ts_now);
882
883 assert_eq!(
884 result.err().map(|reason| reason.to_string()),
885 expected_error.map(|detail| OrderDeniedReason::ValidationFailed {
886 detail: detail.to_string(),
887 }
888 .to_string())
889 );
890 }
891
892 #[rstest]
893 #[case::missing(None)]
894 #[case::zero(Some(UnixNanos::default()))]
895 fn test_validate_limit_expiration_requires_non_zero_expiry(
896 #[case] expire_time: Option<UnixNanos>,
897 ) {
898 let order = with_expire_time(
899 make_limit(false, false, false, TimeInForce::Gtd),
900 expire_time,
901 );
902
903 let error = PolymarketOrderBuilder::validate_limit_expiration(
904 &order,
905 UnixNanos::from(1_700_000_000_900_000_000u64),
906 )
907 .unwrap_err();
908
909 assert_eq!(error, OrderDeniedReason::MissingExpireTime);
910 }
911
912 #[rstest]
913 fn test_validate_limit_order_reduce_only_denied() {
914 let order = make_limit(true, false, false, TimeInForce::Gtc);
915 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
916 assert!(err.to_string().contains("Reduce-only"));
917 }
918
919 #[rstest]
920 fn test_validate_limit_order_quote_quantity_buy_allowed() {
921 let order = make_limit(false, true, false, TimeInForce::Gtc);
922 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
923 }
924
925 #[rstest]
926 fn test_validate_limit_order_quote_quantity_sell_denied() {
927 let order = make_limit_with_quantity(
928 OrderSide::Sell,
929 Quantity::from("10"),
930 false,
931 true,
932 false,
933 TimeInForce::Gtc,
934 Price::from("0.50"),
935 );
936 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
937 assert_eq!(
938 err,
939 validation_failed("Limit SELL orders require quote_quantity=false (amount in shares)")
940 );
941 }
942
943 #[rstest]
944 #[case::base(false, "shares")]
945 #[case::collateral(true, "pUSD")]
946 fn test_validate_limit_order_denies_quantity_below_signing_minimum(
947 #[case] quote_quantity: bool,
948 #[case] denomination: &str,
949 ) {
950 let order = make_limit_with_quantity(
951 OrderSide::Buy,
952 Quantity::from("0.009"),
953 false,
954 quote_quantity,
955 false,
956 TimeInForce::Gtc,
957 Price::from("0.50"),
958 );
959
960 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
961
962 assert_eq!(
963 err,
964 validation_failed(format!(
965 "Polymarket limit order amount 0.009 {denomination} truncates to zero at 2 decimal places"
966 ))
967 );
968 }
969
970 #[rstest]
971 fn test_validate_limit_order_post_only_ioc_denied() {
972 let order = make_limit(false, false, true, TimeInForce::Ioc);
973 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
974 assert!(err.to_string().contains("Post-only"));
975 }
976
977 #[rstest]
978 fn test_validate_limit_order_post_only_gtc_allowed() {
979 let order = make_limit(false, false, true, TimeInForce::Gtc);
980 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
981 }
982
983 #[rstest]
984 fn test_validate_market_order_buy_with_quote_qty() {
985 let order = make_market(OrderSide::Buy, true);
986 assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
987 }
988
989 #[rstest]
990 fn test_validate_market_order_buy_without_quote_qty_denied() {
991 let order = make_market(OrderSide::Buy, false);
992 let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
993 assert!(err.to_string().contains("quote_quantity=true"));
994 }
995
996 #[rstest]
997 fn test_validate_market_order_sell_without_quote_qty() {
998 let order = make_market(OrderSide::Sell, false);
999 assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
1000 }
1001
1002 #[rstest]
1003 fn test_validate_market_order_sell_with_quote_qty_denied() {
1004 let order = make_market(OrderSide::Sell, true);
1005 let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
1006 assert!(err.to_string().contains("quote_quantity=false"));
1007 }
1008
1009 #[rstest]
1010 fn test_validate_market_order_wrong_type_denied() {
1011 let limit = make_limit(false, false, false, TimeInForce::Gtc);
1013 let err = PolymarketOrderBuilder::validate_market_order(&limit).unwrap_err();
1014 assert_eq!(
1015 err,
1016 OrderDeniedReason::UnsupportedOrderType {
1017 order_type: OrderType::Limit,
1018 }
1019 );
1020 }
1021
1022 fn make_test_builder() -> PolymarketOrderBuilder {
1023 let pk = EvmPrivateKey::new(
1024 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
1025 )
1026 .unwrap();
1027 let signer = OrderSigner::new(&pk).unwrap();
1028 let addr = format!("{:#x}", signer.address());
1029 PolymarketOrderBuilder::new(signer, addr.clone(), addr, PolymarketSignatureType::Eoa)
1030 }
1031
1032 #[rstest]
1033 fn test_next_timestamp_ms_is_strictly_monotonic() {
1034 let builder = make_test_builder();
1035 let mut prev = builder.next_timestamp_ms();
1036 for _ in 0..1_000 {
1037 let next = builder.next_timestamp_ms();
1038 assert!(
1039 next > prev,
1040 "timestamp not strictly monotonic: {prev} >= {next}"
1041 );
1042 prev = next;
1043 }
1044 }
1045
1046 #[rstest]
1047 fn test_build_orders_produce_unique_timestamps() {
1048 let builder = make_test_builder();
1049 let mut timestamps = ahash::AHashSet::new();
1050
1051 for _ in 0..50 {
1052 let order = builder
1053 .build_limit_order(
1054 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1055 PolymarketOrderSide::Buy,
1056 dec!(0.50),
1057 dec!(10),
1058 PolymarketOrderType::GTC,
1059 "0",
1060 false,
1061 2,
1062 )
1063 .unwrap();
1064 assert!(
1065 timestamps.insert(order.timestamp.clone()),
1066 "duplicate timestamp {} in burst",
1067 order.timestamp,
1068 );
1069 }
1070 }
1071
1072 #[rstest]
1073 fn test_built_order_carries_nautilus_builder_code() {
1074 let builder = make_test_builder();
1075 let order = builder
1076 .build_limit_order(
1077 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1078 PolymarketOrderSide::Buy,
1079 dec!(0.50),
1080 dec!(10),
1081 PolymarketOrderType::GTC,
1082 "0",
1083 false,
1084 2,
1085 )
1086 .unwrap();
1087 assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
1088 }
1089
1090 #[rstest]
1091 fn test_poly_1271_order_uses_deposit_wallet_as_signer() {
1092 let pk = EvmPrivateKey::new(
1093 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
1094 )
1095 .unwrap();
1096 let signer = OrderSigner::new(&pk).unwrap();
1097 let signer_address = format!("{:#x}", signer.address());
1098 let deposit_wallet = "0x1111111111111111111111111111111111111111".to_string();
1099 let builder = PolymarketOrderBuilder::new(
1100 signer,
1101 signer_address,
1102 deposit_wallet.clone(),
1103 PolymarketSignatureType::Poly1271,
1104 );
1105
1106 let order = builder
1107 .build_limit_order(
1108 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1109 PolymarketOrderSide::Buy,
1110 dec!(0.50),
1111 dec!(10),
1112 PolymarketOrderType::GTC,
1113 "0",
1114 false,
1115 2,
1116 )
1117 .unwrap();
1118
1119 assert_eq!(order.maker, deposit_wallet);
1120 assert_eq!(order.signer, deposit_wallet);
1121 assert_eq!(order.signature_type, PolymarketSignatureType::Poly1271);
1122 assert_eq!(order.signature.expose_secret().len(), 636);
1123 }
1124
1125 #[rstest]
1126 fn test_build_limit_order_expiration_passthrough() {
1127 let builder = make_test_builder();
1128 let order = builder
1129 .build_limit_order(
1130 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1131 PolymarketOrderSide::Buy,
1132 dec!(0.50),
1133 dec!(10),
1134 PolymarketOrderType::GTD,
1135 "1735689600",
1136 false,
1137 2,
1138 )
1139 .unwrap();
1140 assert_eq!(order.expiration, "1735689600");
1141 }
1142
1143 #[rstest]
1144 fn test_build_limit_order_amount_matrix(
1145 #[values(1, 2, 3, 4)] tick_decimals: u32,
1146 #[values(PolymarketOrderSide::Buy, PolymarketOrderSide::Sell)] side: PolymarketOrderSide,
1147 #[values(
1148 PolymarketOrderType::GTC,
1149 PolymarketOrderType::GTD,
1150 PolymarketOrderType::FAK,
1151 PolymarketOrderType::FOK
1152 )]
1153 order_type: PolymarketOrderType,
1154 ) {
1155 let (price, quantity, notional) = match tick_decimals {
1156 1 => (dec!(0.5), dec!(10), dec!(5_000_000)),
1157 2 => (dec!(0.56), dec!(10), dec!(5_600_000)),
1158 3 => (dec!(0.961), dec!(10), dec!(9_610_000)),
1159 4 => (dec!(0.9612), dec!(25), dec!(24_030_000)),
1160 _ => unreachable!(),
1161 };
1162 let quantity_amount = to_fixed_decimal(quantity);
1163 let expiration = if order_type == PolymarketOrderType::GTD {
1164 "1735689600"
1165 } else {
1166 "0"
1167 };
1168 let builder = make_test_builder();
1169
1170 let order = builder
1171 .build_limit_order(
1172 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1173 side,
1174 price,
1175 quantity,
1176 order_type,
1177 expiration,
1178 false,
1179 tick_decimals,
1180 )
1181 .unwrap();
1182
1183 let expected = match side {
1184 PolymarketOrderSide::Buy => (notional, quantity_amount),
1185 PolymarketOrderSide::Sell => (quantity_amount, notional),
1186 };
1187 assert_eq!((order.maker_amount, order.taker_amount), expected);
1188 }
1189
1190 #[rstest]
1191 #[case::cent_tick(dec!(0.56), dec!(10.089), 2, dec!(10_080_000), dec!(18_000_000))]
1192 #[case::half_cent_tick(
1193 dec!(0.505),
1194 dec!(10.109),
1195 3,
1196 dec!(10_100_000),
1197 dec!(20_000_000)
1198 )]
1199 #[case::quarter_cent_tick(
1200 dec!(0.5025),
1201 dec!(10.059),
1202 4,
1203 dec!(10_050_000),
1204 dec!(20_000_000)
1205 )]
1206 #[case::minimum_order_size(dec!(0.99), dec!(4.959), 2, dec!(4_950_000), dec!(5_000_000))]
1207 fn test_build_collateral_limit_buy_amounts(
1208 #[case] price: Decimal,
1209 #[case] amount: Decimal,
1210 #[case] tick_decimals: u32,
1211 #[case] expected_maker: Decimal,
1212 #[case] expected_taker: Decimal,
1213 ) {
1214 let builder = make_test_builder();
1215
1216 let order = builder
1217 .build_limit_order_from_collateral(
1218 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1219 price,
1220 amount,
1221 "0",
1222 false,
1223 tick_decimals,
1224 )
1225 .unwrap();
1226
1227 assert_eq!(order.side, PolymarketOrderSide::Buy);
1228 assert_eq!(order.maker_amount, expected_maker);
1229 assert_eq!(order.taker_amount, expected_taker);
1230 }
1231
1232 #[rstest]
1233 #[case::zero_after_quantization(dec!(0.50), dec!(0.009), 2, "amount 0.009 pUSD truncates to zero")]
1234 #[case::zero_price(dec!(0), dec!(1), 2, "price must be positive")]
1235 #[case::inexact_signed_price(
1236 dec!(0.989),
1237 dec!(5),
1238 3,
1239 "amount 5 pUSD cannot preserve limit price 0.989 after venue quantization"
1240 )]
1241 fn test_build_collateral_limit_buy_denies_invalid_amounts(
1242 #[case] price: Decimal,
1243 #[case] amount: Decimal,
1244 #[case] tick_decimals: u32,
1245 #[case] expected_error: &str,
1246 ) {
1247 let builder = make_test_builder();
1248
1249 let error = builder
1250 .build_limit_order_from_collateral(
1251 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1252 price,
1253 amount,
1254 "0",
1255 false,
1256 tick_decimals,
1257 )
1258 .unwrap_err();
1259
1260 assert!(
1261 error.to_string().contains(expected_error),
1262 "unexpected error: {error}"
1263 );
1264 }
1265
1266 #[rstest]
1267 fn test_build_limit_order_denies_unrepresentable_immediate_buy(
1268 #[values(1, 2, 3, 4)] tick_decimals: u32,
1269 #[values(PolymarketOrderType::FAK, PolymarketOrderType::FOK)]
1270 order_type: PolymarketOrderType,
1271 ) {
1272 let (price, quantity, maker_amount) = match tick_decimals {
1273 1 => (dec!(0.5), dec!(1.01), dec!(0.505)),
1274 2 => (dec!(0.56), dec!(1.01), dec!(0.5656)),
1275 3 => (dec!(0.961), dec!(5), dec!(4.805)),
1276 4 => (dec!(0.9612), dec!(5), dec!(4.806)),
1277 _ => unreachable!(),
1278 };
1279 let builder = make_test_builder();
1280
1281 let error = builder
1282 .build_limit_order(
1283 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1284 PolymarketOrderSide::Buy,
1285 price,
1286 quantity,
1287 order_type,
1288 "0",
1289 false,
1290 tick_decimals,
1291 )
1292 .unwrap_err();
1293
1294 assert_eq!(
1295 error.to_string(),
1296 format!(
1297 "Polymarket {order_type} BUY maker amount {maker_amount} pUSD exceeds 2 decimal places for price {price} and quantity {quantity}"
1298 )
1299 );
1300 }
1301
1302 #[rstest]
1303 #[case::gtc_buy(
1304 PolymarketOrderType::GTC,
1305 PolymarketOrderSide::Buy,
1306 dec!(4_805_000),
1307 dec!(5_000_000),
1308 )]
1309 #[case::gtd_buy(
1310 PolymarketOrderType::GTD,
1311 PolymarketOrderSide::Buy,
1312 dec!(4_805_000),
1313 dec!(5_000_000),
1314 )]
1315 #[case::fak_sell(
1316 PolymarketOrderType::FAK,
1317 PolymarketOrderSide::Sell,
1318 dec!(5_000_000),
1319 dec!(4_805_000),
1320 )]
1321 #[case::fok_sell(
1322 PolymarketOrderType::FOK,
1323 PolymarketOrderSide::Sell,
1324 dec!(5_000_000),
1325 dec!(4_805_000),
1326 )]
1327 fn test_build_limit_order_preserves_unaffected_fractional_cent_amounts(
1328 #[case] order_type: PolymarketOrderType,
1329 #[case] side: PolymarketOrderSide,
1330 #[case] expected_maker: Decimal,
1331 #[case] expected_taker: Decimal,
1332 ) {
1333 let builder = make_test_builder();
1334
1335 let order = builder
1336 .build_limit_order(
1337 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1338 side,
1339 dec!(0.961),
1340 dec!(5),
1341 order_type,
1342 "0",
1343 false,
1344 3,
1345 )
1346 .unwrap();
1347
1348 assert_eq!(order.maker_amount, expected_maker);
1349 assert_eq!(order.taker_amount, expected_taker);
1350 }
1351
1352 #[rstest]
1353 fn test_validate_limit_order_gtd_with_expire_accepted() {
1354 let order = make_limit(false, false, false, TimeInForce::Gtd);
1360 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
1361 }
1362
1363 #[rstest]
1364 fn test_build_market_buy_order_wire_shape() {
1365 let builder = make_test_builder();
1368 let order = builder
1369 .build_market_order(
1370 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1371 PolymarketOrderSide::Buy,
1372 dec!(0.50),
1373 dec!(10),
1374 false,
1375 2,
1376 )
1377 .unwrap();
1378
1379 assert_eq!(order.expiration, "0");
1380 assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
1381 assert_eq!(order.metadata, ZERO_BYTES32);
1382 assert_eq!(order.side, PolymarketOrderSide::Buy);
1383 assert!(!order.timestamp.is_empty());
1384 assert!(!order.signature.expose_secret().is_empty());
1385 assert_eq!(order.maker_amount, dec!(10_000_000));
1388 assert_eq!(order.taker_amount, dec!(20_000_000));
1389 }
1390
1391 #[rstest]
1392 fn test_build_market_buy_order_rejects_fee_adjusted_amount_below_lot_size() {
1393 let builder = make_test_builder();
1394 let adjusted = adjust_market_buy_amount(
1395 dec!(10),
1396 dec!(0.009),
1397 dec!(0.5),
1398 dec!(0.04),
1399 dec!(1),
1400 dec!(0),
1401 )
1402 .unwrap();
1403 assert_eq!(adjusted, dec!(0.008823));
1404
1405 let err = builder
1406 .build_market_order(
1407 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1408 PolymarketOrderSide::Buy,
1409 dec!(0.5),
1410 adjusted,
1411 false,
1412 2,
1413 )
1414 .unwrap_err();
1415
1416 assert_eq!(
1417 err.to_string(),
1418 "Polymarket market BUY amount 0.008823 pUSD truncates to zero at 2 decimal places",
1419 );
1420 assert_eq!(builder.last_timestamp_ms.load(Ordering::Relaxed), 0);
1421 }
1422
1423 #[rstest]
1424 fn test_build_market_sell_order_wire_shape() {
1425 let builder = make_test_builder();
1428 let order = builder
1429 .build_market_order(
1430 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1431 PolymarketOrderSide::Sell,
1432 dec!(0.50),
1433 dec!(20),
1434 false,
1435 2,
1436 )
1437 .unwrap();
1438
1439 assert_eq!(order.expiration, "0");
1440 assert_eq!(order.side, PolymarketOrderSide::Sell);
1441 assert_eq!(order.maker_amount, dec!(20_000_000));
1442 assert_eq!(order.taker_amount, dec!(10_000_000));
1443 assert!(!order.signature.expose_secret().is_empty());
1444 }
1445
1446 #[rstest]
1447 fn test_generate_salt_uniqueness() {
1448 let s1 = generate_salt();
1449 let s2 = generate_salt();
1450 assert_ne!(s1, s2);
1451 }
1452
1453 #[rstest]
1454 fn test_generate_salt_within_53_bits() {
1455 for _ in 0..100 {
1456 let s = generate_salt();
1457 assert!(s < (1u64 << 53));
1458 }
1459 }
1460
1461 #[rstest]
1464 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
1465 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
1466 #[case(dec!(0.75), dec!(200), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
1467 #[case(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3, dec!(13_296_150), dec!(23_450_000))]
1469 #[case(dec!(0.55), dec!(10), PolymarketOrderSide::Buy, 1, dec!(5_500_000), dec!(10_000_000))]
1470 fn test_compute_maker_taker_amounts(
1471 #[case] price: Decimal,
1472 #[case] quantity: Decimal,
1473 #[case] side: PolymarketOrderSide,
1474 #[case] tick_decimals: u32,
1475 #[case] expected_maker: Decimal,
1476 #[case] expected_taker: Decimal,
1477 ) {
1478 let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
1479 assert_eq!(maker, expected_maker);
1480 assert_eq!(taker, expected_taker);
1481 }
1482
1483 #[rstest]
1490 #[case::buy_tick_tenth(
1492 dec!(0.5), dec!(21.04), PolymarketOrderSide::Buy, 1,
1493 dec!(10_520_000), dec!(21_040_000),
1494 )]
1495 #[case::sell_tick_tenth(
1496 dec!(0.5), dec!(21.04), PolymarketOrderSide::Sell, 1,
1497 dec!(21_040_000), dec!(10_520_000),
1498 )]
1499 #[case::buy_tick_hundredth(
1501 dec!(0.56), dec!(21.04), PolymarketOrderSide::Buy, 2,
1502 dec!(11_782_400), dec!(21_040_000),
1503 )]
1504 #[case::sell_tick_hundredth(
1505 dec!(0.56), dec!(21.04), PolymarketOrderSide::Sell, 2,
1506 dec!(21_040_000), dec!(11_782_400),
1507 )]
1508 #[case::buy_decimal_accuracy_24(
1509 dec!(0.24), dec!(15), PolymarketOrderSide::Buy, 2,
1510 dec!(3_600_000), dec!(15_000_000),
1511 )]
1512 #[case::buy_decimal_accuracy_82(
1513 dec!(0.82), dec!(101), PolymarketOrderSide::Buy, 2,
1514 dec!(82_820_000), dec!(101_000_000),
1515 )]
1516 #[case::buy_decimal_accuracy_18233(
1517 dec!(0.58), dec!(18233.33), PolymarketOrderSide::Buy, 2,
1518 dec!(10_575_331_400), dec!(18_233_330_000),
1519 )]
1520 #[case::buy_tick_thousandth(
1522 dec!(0.056), dec!(21.04), PolymarketOrderSide::Buy, 3,
1523 dec!(1_178_240), dec!(21_040_000),
1524 )]
1525 #[case::sell_tick_thousandth(
1526 dec!(0.056), dec!(21.04), PolymarketOrderSide::Sell, 3,
1527 dec!(21_040_000), dec!(1_178_240),
1528 )]
1529 #[case::buy_tick_ten_thousandth(
1531 dec!(0.0056), dec!(21.04), PolymarketOrderSide::Buy, 4,
1532 dec!(117_824), dec!(21_040_000),
1533 )]
1534 #[case::sell_tick_ten_thousandth(
1535 dec!(0.0056), dec!(21.04), PolymarketOrderSide::Sell, 4,
1536 dec!(21_040_000), dec!(117_824),
1537 )]
1538 fn test_compute_maker_taker_amounts_sdk_parity(
1539 #[case] price: Decimal,
1540 #[case] quantity: Decimal,
1541 #[case] side: PolymarketOrderSide,
1542 #[case] tick_decimals: u32,
1543 #[case] expected_maker: Decimal,
1544 #[case] expected_taker: Decimal,
1545 ) {
1546 let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
1547 assert_eq!(maker, expected_maker);
1548 assert_eq!(taker, expected_taker);
1549 }
1550
1551 #[rstest]
1554 #[case(dec!(0.50), dec!(50), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
1555 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
1556 #[case(dec!(0.75), dec!(150), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
1557 #[case(dec!(0.96), dec!(5.208), PolymarketOrderSide::Sell, 2, dec!(5_200_000), dec!(4_992_000))]
1558 #[case(dec!(0.211), dec!(23.696681), PolymarketOrderSide::Sell, 3, dec!(23_690_000), dec!(4_998_590))]
1560 fn test_compute_market_maker_taker_amounts(
1561 #[case] price: Decimal,
1562 #[case] amount: Decimal,
1563 #[case] side: PolymarketOrderSide,
1564 #[case] tick_decimals: u32,
1565 #[case] expected_maker: Decimal,
1566 #[case] expected_taker: Decimal,
1567 ) {
1568 let (maker, taker) = compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
1569 assert_eq!(maker, expected_maker);
1570 assert_eq!(taker, expected_taker);
1571 }
1572
1573 #[rstest]
1574 fn test_compute_maker_taker_qty_truncated_to_lot_size() {
1575 let (maker, taker) =
1578 compute_maker_taker_amounts(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3);
1579 assert_eq!(maker, dec!(13_296_150));
1580 assert_eq!(taker, dec!(23_450_000));
1581 }
1582
1583 #[rstest]
1584 fn test_compute_maker_taker_zero_amounts() {
1585 let (maker, taker) =
1586 compute_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
1587 assert_eq!(maker, dec!(0));
1588 assert_eq!(taker, dec!(0));
1589 }
1590
1591 #[rstest]
1592 fn test_compute_maker_taker_tick_decimals_1() {
1593 let (maker, taker) =
1594 compute_maker_taker_amounts(dec!(0.3), dec!(50), PolymarketOrderSide::Buy, 1);
1595 assert_eq!(maker, dec!(15_000_000));
1596 assert_eq!(taker, dec!(50_000_000));
1597 }
1598
1599 #[rstest]
1600 fn test_compute_maker_taker_tick_decimals_3_sell() {
1601 let (maker, taker) =
1604 compute_maker_taker_amounts(dec!(0.789), dec!(15.123), PolymarketOrderSide::Sell, 3);
1605 assert_eq!(maker, dec!(15_120_000));
1606 assert_eq!(taker, dec!(11_929_680));
1607 }
1608
1609 #[rstest]
1610 fn test_compute_market_maker_taker_zero_amount() {
1611 let (maker, taker) =
1612 compute_market_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
1613 assert_eq!(maker, dec!(0));
1614 assert_eq!(taker, dec!(0));
1615 }
1616
1617 #[rstest]
1618 fn test_compute_market_sell_position_close() {
1619 let (maker, taker) = compute_market_maker_taker_amounts(
1623 dec!(0.211),
1624 dec!(23.696681),
1625 PolymarketOrderSide::Sell,
1626 3,
1627 );
1628 assert_eq!(maker, dec!(23_690_000));
1629 assert_eq!(taker, dec!(4_998_590));
1631 assert_eq!(maker % dec!(10_000), dec!(0));
1633 assert_eq!(taker % dec!(10), dec!(0));
1635 }
1636
1637 #[rstest]
1638 fn test_to_fixed_decimal_basic() {
1639 assert_eq!(to_fixed_decimal(dec!(13.29955)), dec!(13_299_550));
1640 assert_eq!(to_fixed_decimal(dec!(100)), dec!(100_000_000));
1641 assert_eq!(to_fixed_decimal(dec!(0)), dec!(0));
1642 }
1643}