1use std::sync::atomic::{AtomicU64, Ordering};
31
32use nautilus_core::{UnixNanos, 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, SignatureType},
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: SignatureType,
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: SignatureType,
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 fn build_market_order(
150 &self,
151 token_id: &str,
152 side: PolymarketOrderSide,
153 price: Decimal,
154 amount: Decimal,
155 neg_risk: bool,
156 tick_decimals: u32,
157 ) -> anyhow::Result<PolymarketOrder> {
158 if side == PolymarketOrderSide::Buy && amount.trunc_with_scale(LOT_SIZE_SCALE).is_zero() {
159 anyhow::bail!(
160 "Polymarket market BUY amount {} pUSD truncates to zero at {LOT_SIZE_SCALE} decimal places",
161 amount.normalize(),
162 );
163 }
164
165 let (maker_amount, taker_amount) =
166 compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
167 self.build_and_sign(token_id, side, maker_amount, taker_amount, "0", neg_risk)
168 }
169
170 pub fn expected_order_id(
172 &self,
173 order: &PolymarketOrder,
174 neg_risk: bool,
175 ) -> anyhow::Result<VenueOrderId> {
176 let hash = order_hash(order, neg_risk)
177 .map_err(|e| anyhow::anyhow!("Failed to derive order hash: {e}"))?;
178 let order_id = format!("{hash:#x}");
179 Ok(VenueOrderId::from(order_id.as_str()))
180 }
181
182 pub fn validate_limit_order(order: &OrderAny) -> Result<(), OrderDeniedReason> {
184 if order.is_reduce_only() {
185 return Err(validation_failed(
186 "Reduce-only orders not supported on Polymarket",
187 ));
188 }
189
190 if order.order_type() != OrderType::Limit {
191 return Err(OrderDeniedReason::UnsupportedOrderType {
192 order_type: order.order_type(),
193 });
194 }
195
196 if order.is_quote_quantity() {
197 return Err(validation_failed(
198 "Quote quantity not supported for limit orders",
199 ));
200 }
201
202 let price = order
203 .price()
204 .ok_or_else(|| validation_failed("Limit orders require a price"))?;
205
206 let order_type = PolymarketOrderType::try_from(order.time_in_force())
207 .map_err(|_| OrderDeniedReason::UnsupportedTimeInForce(order.time_in_force()))?;
208
209 let side = PolymarketOrderSide::from(order.order_side());
210
211 if order.is_post_only()
212 && !matches!(order.time_in_force(), TimeInForce::Gtc | TimeInForce::Gtd)
213 {
214 return Err(validation_failed(
215 "Post-only orders require GTC or GTD time in force",
216 ));
217 }
218
219 validate_immediate_buy_maker_amount(
220 price.as_decimal(),
221 order.quantity().as_decimal(),
222 side,
223 order_type,
224 )
225 .map_err(validation_failed)?;
226
227 Ok(())
228 }
229
230 pub(crate) fn validate_limit_price(
231 order: &OrderAny,
232 tick_size: Price,
233 ) -> Result<(), OrderDeniedReason> {
234 let price = order
235 .price()
236 .ok_or_else(|| validation_failed("Limit orders require a price"))?;
237 let tick = tick_size.as_decimal();
238 let price_decimal = price.as_decimal();
239
240 validate_price(price_decimal, tick, "Limit order price", &price.to_string())
241 .map_err(validation_failed)
242 }
243
244 pub(crate) fn validate_limit_expiration(
245 order: &OrderAny,
246 ts_now: UnixNanos,
247 ) -> Result<(), OrderDeniedReason> {
248 if order.time_in_force() != TimeInForce::Gtd {
249 return Ok(());
250 }
251
252 let expire_time = order
253 .expire_time()
254 .filter(|expire_time| !expire_time.is_zero())
255 .ok_or(OrderDeniedReason::MissingExpireTime)?;
256 let now_secs = ts_now.as_seconds();
257 let expire_secs = expire_time.as_seconds();
258 let minimum_expire_secs = now_secs.saturating_add(GTD_EXPIRATION_BUFFER_SECS);
259
260 if expire_secs < minimum_expire_secs {
261 return Err(validation_failed(format!(
262 "Polymarket GTD expiry must be at least {GTD_EXPIRATION_BUFFER_SECS} seconds in the future"
263 )));
264 }
265
266 Ok(())
267 }
268
269 pub fn validate_market_order(order: &OrderAny) -> Result<(), OrderDeniedReason> {
271 if order.is_reduce_only() {
272 return Err(validation_failed(
273 "Reduce-only orders not supported on Polymarket",
274 ));
275 }
276
277 if order.order_type() != OrderType::Market {
278 return Err(OrderDeniedReason::UnsupportedOrderType {
279 order_type: order.order_type(),
280 });
281 }
282
283 match order.order_side() {
286 OrderSide::Buy => {
287 if !order.is_quote_quantity() {
288 return Err(validation_failed(
289 "Market BUY orders require quote_quantity=true (amount in pUSD)",
290 ));
291 }
292 }
293 OrderSide::Sell => {
294 if order.is_quote_quantity() {
295 return Err(validation_failed(
296 "Market SELL orders require quote_quantity=false (amount in shares)",
297 ));
298 }
299 }
300 }
301
302 if PolymarketOrderType::from_market_time_in_force(order.time_in_force()).is_err() {
303 return Err(OrderDeniedReason::UnsupportedTimeInForce(
304 order.time_in_force(),
305 ));
306 }
307
308 Ok(())
309 }
310
311 pub(crate) fn normalize_market_price(
312 price: Decimal,
313 tick_size: Price,
314 tick_decimals: u32,
315 ) -> Result<Decimal, String> {
316 let tick = tick_size.as_decimal();
317 let price = price.trunc_with_scale(tick_decimals);
318
319 validate_price(
320 price,
321 tick,
322 "Derived market price",
323 &price.normalize().to_string(),
324 )?;
325
326 Ok(price)
327 }
328
329 fn build_and_sign(
330 &self,
331 token_id: &str,
332 side: PolymarketOrderSide,
333 maker_amount: Decimal,
334 taker_amount: Decimal,
335 expiration: &str,
336 neg_risk: bool,
337 ) -> anyhow::Result<PolymarketOrder> {
338 let salt = generate_salt();
339 let timestamp_ms = self.next_timestamp_ms();
340
341 let mut poly_order = PolymarketOrder {
342 salt,
343 maker: self.maker_address.clone(),
344 signer: self.order_signer_address(),
345 token_id: Ustr::from(token_id),
346 maker_amount,
347 taker_amount,
348 side,
349 signature_type: self.signature_type,
350 expiration: expiration.to_string(),
351 timestamp: timestamp_ms.to_string(),
352 metadata: ZERO_BYTES32.to_string(),
353 builder: POLYMARKET_NAUTILUS_BUILDER_CODE.to_string(),
354 signature: String::new(),
355 };
356
357 let signature = self
358 .order_signer
359 .sign_order(&poly_order, neg_risk)
360 .map_err(|e| anyhow::anyhow!("EIP-712 signing failed: {e}"))?;
361 poly_order.signature = signature;
362
363 Ok(poly_order)
364 }
365
366 fn order_signer_address(&self) -> String {
367 match self.signature_type {
368 SignatureType::Poly1271 => self.maker_address.clone(),
369 _ => self.signer_address.clone(),
370 }
371 }
372}
373
374fn validation_failed(detail: impl Into<String>) -> OrderDeniedReason {
375 OrderDeniedReason::ValidationFailed {
376 detail: detail.into(),
377 }
378}
379
380fn validate_price(
381 price: Decimal,
382 tick: Decimal,
383 label: &str,
384 price_display: &str,
385) -> Result<(), String> {
386 let tick = tick.normalize();
387 let max_price = Decimal::ONE - tick;
388
389 if price < tick || price > max_price {
390 return Err(format!(
391 "{label} {price_display} outside Polymarket range [{tick}, {max_price}]"
392 ));
393 }
394
395 if price % tick != Decimal::ZERO {
396 return Err(format!(
397 "{label} {price_display} does not conform to Polymarket tick size {tick}"
398 ));
399 }
400
401 Ok(())
402}
403
404fn to_fixed_decimal(d: Decimal) -> Decimal {
405 let mantissa = d.normalize().trunc_with_scale(USDC_DECIMALS).mantissa();
406 Decimal::from(mantissa)
407}
408
409pub(crate) fn signed_limit_order_quantity(quantity: Decimal) -> Decimal {
411 quantity.trunc_with_scale(LOT_SIZE_SCALE)
412}
413
414fn validate_immediate_buy_maker_amount(
415 price: Decimal,
416 quantity: Decimal,
417 side: PolymarketOrderSide,
418 order_type: PolymarketOrderType,
419) -> Result<(), String> {
420 if side != PolymarketOrderSide::Buy
421 || !matches!(
422 order_type,
423 PolymarketOrderType::FAK | PolymarketOrderType::FOK
424 )
425 {
426 return Ok(());
427 }
428
429 let quantity = signed_limit_order_quantity(quantity);
430 let maker_amount = (quantity * price).normalize();
431 if maker_amount.scale() > LOT_SIZE_SCALE {
432 return Err(format!(
433 "Polymarket {order_type} BUY maker amount {} pUSD exceeds 2 decimal places for price {} and quantity {}",
434 maker_amount,
435 price.normalize(),
436 quantity.normalize(),
437 ));
438 }
439
440 Ok(())
441}
442
443pub fn compute_maker_taker_amounts(
452 price: Decimal,
453 quantity: Decimal,
454 side: PolymarketOrderSide,
455 tick_decimals: u32,
456) -> (Decimal, Decimal) {
457 let precision = tick_decimals + LOT_SIZE_SCALE;
458 let qty = signed_limit_order_quantity(quantity);
459
460 match side {
461 PolymarketOrderSide::Buy => {
462 let maker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
463 let taker_amount = to_fixed_decimal(qty);
464 (maker_amount, taker_amount)
465 }
466 PolymarketOrderSide::Sell => {
467 let maker_amount = to_fixed_decimal(qty);
468 let taker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
469 (maker_amount, taker_amount)
470 }
471 }
472}
473
474pub fn compute_market_maker_taker_amounts(
484 price: Decimal,
485 amount: Decimal,
486 side: PolymarketOrderSide,
487 tick_decimals: u32,
488) -> (Decimal, Decimal) {
489 let precision = tick_decimals + LOT_SIZE_SCALE;
490 let amt = amount.trunc_with_scale(LOT_SIZE_SCALE);
491
492 match side {
493 PolymarketOrderSide::Buy => {
494 let maker_amount = to_fixed_decimal(amt);
495 let taker_amount = to_fixed_decimal((amt / price).trunc_with_scale(precision));
496 (maker_amount, taker_amount)
497 }
498 PolymarketOrderSide::Sell => {
499 let maker_amount = to_fixed_decimal(amt);
500 let taker_amount = to_fixed_decimal((amt * price).trunc_with_scale(precision));
501 (maker_amount, taker_amount)
502 }
503 }
504}
505
506pub fn generate_salt() -> u64 {
512 let bytes = uuid::Uuid::new_v4().into_bytes();
513 u64::from_le_bytes(bytes[..8].try_into().unwrap()) & ((1u64 << 53) - 1)
514}
515
516#[cfg(test)]
517mod tests {
518 use nautilus_core::UUID4;
519 use nautilus_model::{
520 enums::{OrderSide, OrderType, TimeInForce},
521 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
522 orders::{LimitOrder, MarketOrder, OrderAny},
523 types::{Price, Quantity},
524 };
525 use rstest::rstest;
526 use rust_decimal::Decimal;
527 use rust_decimal_macros::dec;
528
529 use super::*;
530 use crate::{
531 common::{credential::EvmPrivateKey, enums::PolymarketOrderSide},
532 execution::parse::adjust_market_buy_amount,
533 signing::eip712::OrderSigner,
534 };
535
536 fn make_limit(
537 reduce_only: bool,
538 quote_quantity: bool,
539 post_only: bool,
540 tif: TimeInForce,
541 ) -> OrderAny {
542 make_limit_at_price(
543 reduce_only,
544 quote_quantity,
545 post_only,
546 tif,
547 Price::from("0.50"),
548 )
549 }
550
551 fn make_limit_at_price(
552 reduce_only: bool,
553 quote_quantity: bool,
554 post_only: bool,
555 tif: TimeInForce,
556 price: Price,
557 ) -> OrderAny {
558 let expire_time = if tif == TimeInForce::Gtd {
559 Some(UnixNanos::from(2_000_000_000_000_000_000u64))
560 } else {
561 None
562 };
563 OrderAny::Limit(LimitOrder::new(
564 TraderId::from("TESTER-001"),
565 StrategyId::from("S-001"),
566 InstrumentId::from("TEST.POLYMARKET"),
567 ClientOrderId::from("O-001"),
568 OrderSide::Buy,
569 Quantity::from("10"),
570 price,
571 tif,
572 expire_time,
573 post_only,
574 reduce_only,
575 quote_quantity,
576 None,
577 None,
578 None,
579 None,
580 None,
581 None,
582 None,
583 None,
584 None,
585 None,
586 None,
587 UUID4::new(),
588 UnixNanos::default(),
589 ))
590 }
591
592 fn with_expire_time(mut order: OrderAny, expire_time: Option<UnixNanos>) -> OrderAny {
593 let OrderAny::Limit(limit) = &mut order else {
594 unreachable!("test order is limit")
595 };
596 limit.expire_time = expire_time;
597 order
598 }
599
600 fn make_market(side: OrderSide, quote_quantity: bool) -> OrderAny {
601 OrderAny::Market(MarketOrder::new(
602 TraderId::from("TESTER-001"),
603 StrategyId::from("S-001"),
604 InstrumentId::from("TEST.POLYMARKET"),
605 ClientOrderId::from("O-001"),
606 side,
607 Quantity::from("10"),
608 TimeInForce::Ioc,
609 UUID4::new(),
610 UnixNanos::default(),
611 false,
612 quote_quantity,
613 None,
614 None,
615 None,
616 None,
617 None,
618 None,
619 None,
620 None,
621 ))
622 }
623
624 #[rstest]
625 fn test_validate_limit_order_valid() {
626 let order = make_limit(false, false, false, TimeInForce::Gtc);
627 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
628 }
629
630 #[rstest]
631 #[case("0.000", "0.001", "0.001", "0.999")]
632 #[case("1.000", "0.001", "0.001", "0.999")]
633 #[case("0.0000", "0.0001", "0.0001", "0.9999")]
634 #[case("1.0000", "0.0001", "0.0001", "0.9999")]
635 fn test_validate_limit_order_price_out_of_range_denied(
636 #[case] price: &str,
637 #[case] tick_size: &str,
638 #[case] min_price: &str,
639 #[case] max_price: &str,
640 ) {
641 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
642 let err = PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size))
643 .unwrap_err();
644 assert_eq!(
645 err,
646 OrderDeniedReason::ValidationFailed {
647 detail: format!(
648 "Limit order price {price} outside Polymarket range [{min_price}, {max_price}]"
649 ),
650 }
651 );
652 }
653
654 #[rstest]
655 #[case("0.001", "0.001")]
656 #[case("0.999", "0.001")]
657 #[case("0.0001", "0.0001")]
658 #[case("0.9999", "0.0001")]
659 #[case("0.005", "0.005")]
660 #[case("0.995", "0.005")]
661 #[case("0.0025", "0.0025")]
662 #[case("0.9975", "0.0025")]
663 fn test_validate_limit_order_price_boundary_allowed(
664 #[case] price: &str,
665 #[case] tick_size: &str,
666 ) {
667 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
668 assert!(
669 PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size)).is_ok()
670 );
671 }
672
673 #[rstest]
674 #[case("0.505", "0.005")]
675 #[case("0.5025", "0.0025")]
676 fn test_validate_limit_order_price_aligned_allowed(
677 #[case] price: &str,
678 #[case] tick_size: &str,
679 ) {
680 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
681 assert!(
682 PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size)).is_ok()
683 );
684 }
685
686 #[rstest]
687 #[case("0.501", "0.005")]
688 #[case("0.501", "0.0025")]
689 fn test_validate_limit_order_price_misaligned_denied(
690 #[case] price: &str,
691 #[case] tick_size: &str,
692 ) {
693 let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
694 let error = PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size))
695 .unwrap_err();
696 assert_eq!(
697 error,
698 OrderDeniedReason::ValidationFailed {
699 detail: format!(
700 "Limit order price {price} does not conform to Polymarket tick size {tick_size}"
701 ),
702 }
703 );
704 }
705
706 #[rstest]
707 #[case::half_cent("0.5059", "0.005", 3, dec!(0.505))]
708 #[case::half_cent_p4("0.5059", "0.0050", 3, dec!(0.505))]
709 #[case::quarter_cent("0.50259", "0.0025", 4, dec!(0.5025))]
710 fn test_normalize_market_price(
711 #[case] price: &str,
712 #[case] tick_size: &str,
713 #[case] tick_decimals: u32,
714 #[case] expected: Decimal,
715 ) {
716 let normalized = PolymarketOrderBuilder::normalize_market_price(
717 price.parse::<Decimal>().unwrap(),
718 Price::from(tick_size),
719 tick_decimals,
720 )
721 .unwrap();
722
723 assert_eq!(normalized, expected);
724 }
725
726 #[rstest]
727 #[case::below_range(
728 "0.0049",
729 "0.005",
730 "Derived market price 0.004 outside Polymarket range [0.005, 0.995]"
731 )]
732 #[case::above_range(
733 "1.0000",
734 "0.0025",
735 "Derived market price 1 outside Polymarket range [0.0025, 0.9975]"
736 )]
737 #[case::off_half_cent(
738 "0.5019",
739 "0.005",
740 "Derived market price 0.501 does not conform to Polymarket tick size 0.005"
741 )]
742 #[case::off_quarter_cent(
743 "0.50129",
744 "0.0025",
745 "Derived market price 0.5012 does not conform to Polymarket tick size 0.0025"
746 )]
747 fn test_normalize_market_price_denied(
748 #[case] price: &str,
749 #[case] tick_size: &str,
750 #[case] expected: &str,
751 ) {
752 let tick_size = Price::from(tick_size);
753 let error = PolymarketOrderBuilder::normalize_market_price(
754 price.parse().unwrap(),
755 tick_size,
756 u32::from(tick_size.precision),
757 )
758 .unwrap_err();
759
760 assert_eq!(error, expected);
761 }
762
763 #[rstest]
764 #[case::below(
765 1_700_000_179_999_999_999,
766 Some("Polymarket GTD expiry must be at least 180 seconds in the future")
767 )]
768 #[case::equal(1_700_000_180_000_000_000, None)]
769 #[case::above(1_700_000_181_000_000_000, None)]
770 fn test_validate_limit_expiration_boundary(
771 #[case] expire_ns: u64,
772 #[case] expected_error: Option<&str>,
773 ) {
774 let order = with_expire_time(
775 make_limit(false, false, false, TimeInForce::Gtd),
776 Some(UnixNanos::from(expire_ns)),
777 );
778 let ts_now = UnixNanos::from(1_700_000_000_900_000_000u64);
779
780 let result = PolymarketOrderBuilder::validate_limit_expiration(&order, ts_now);
781
782 assert_eq!(
783 result.err().map(|reason| reason.to_string()),
784 expected_error.map(|detail| OrderDeniedReason::ValidationFailed {
785 detail: detail.to_string(),
786 }
787 .to_string())
788 );
789 }
790
791 #[rstest]
792 #[case::missing(None)]
793 #[case::zero(Some(UnixNanos::default()))]
794 fn test_validate_limit_expiration_requires_non_zero_expiry(
795 #[case] expire_time: Option<UnixNanos>,
796 ) {
797 let order = with_expire_time(
798 make_limit(false, false, false, TimeInForce::Gtd),
799 expire_time,
800 );
801
802 let error = PolymarketOrderBuilder::validate_limit_expiration(
803 &order,
804 UnixNanos::from(1_700_000_000_900_000_000u64),
805 )
806 .unwrap_err();
807
808 assert_eq!(error, OrderDeniedReason::MissingExpireTime);
809 }
810
811 #[rstest]
812 fn test_validate_limit_order_reduce_only_denied() {
813 let order = make_limit(true, false, false, TimeInForce::Gtc);
814 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
815 assert!(err.to_string().contains("Reduce-only"));
816 }
817
818 #[rstest]
819 fn test_validate_limit_order_quote_quantity_denied() {
820 let order = make_limit(false, true, false, TimeInForce::Gtc);
821 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
822 assert!(err.to_string().contains("Quote quantity"));
823 }
824
825 #[rstest]
826 fn test_validate_limit_order_post_only_ioc_denied() {
827 let order = make_limit(false, false, true, TimeInForce::Ioc);
828 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
829 assert!(err.to_string().contains("Post-only"));
830 }
831
832 #[rstest]
833 fn test_validate_limit_order_post_only_gtc_allowed() {
834 let order = make_limit(false, false, true, TimeInForce::Gtc);
835 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
836 }
837
838 #[rstest]
839 fn test_validate_market_order_buy_with_quote_qty() {
840 let order = make_market(OrderSide::Buy, true);
841 assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
842 }
843
844 #[rstest]
845 fn test_validate_market_order_buy_without_quote_qty_denied() {
846 let order = make_market(OrderSide::Buy, false);
847 let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
848 assert!(err.to_string().contains("quote_quantity=true"));
849 }
850
851 #[rstest]
852 fn test_validate_market_order_sell_without_quote_qty() {
853 let order = make_market(OrderSide::Sell, false);
854 assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
855 }
856
857 #[rstest]
858 fn test_validate_market_order_sell_with_quote_qty_denied() {
859 let order = make_market(OrderSide::Sell, true);
860 let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
861 assert!(err.to_string().contains("quote_quantity=false"));
862 }
863
864 #[rstest]
865 fn test_validate_market_order_wrong_type_denied() {
866 let limit = make_limit(false, false, false, TimeInForce::Gtc);
868 let err = PolymarketOrderBuilder::validate_market_order(&limit).unwrap_err();
869 assert_eq!(
870 err,
871 OrderDeniedReason::UnsupportedOrderType {
872 order_type: OrderType::Limit,
873 }
874 );
875 }
876
877 fn make_test_builder() -> PolymarketOrderBuilder {
878 let pk = EvmPrivateKey::new(
879 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
880 )
881 .unwrap();
882 let signer = OrderSigner::new(&pk).unwrap();
883 let addr = format!("{:#x}", signer.address());
884 PolymarketOrderBuilder::new(signer, addr.clone(), addr, SignatureType::Eoa)
885 }
886
887 #[rstest]
888 fn test_next_timestamp_ms_is_strictly_monotonic() {
889 let builder = make_test_builder();
890 let mut prev = builder.next_timestamp_ms();
891 for _ in 0..1_000 {
892 let next = builder.next_timestamp_ms();
893 assert!(
894 next > prev,
895 "timestamp not strictly monotonic: {prev} >= {next}"
896 );
897 prev = next;
898 }
899 }
900
901 #[rstest]
902 fn test_build_orders_produce_unique_timestamps() {
903 let builder = make_test_builder();
904 let mut timestamps = ahash::AHashSet::new();
905
906 for _ in 0..50 {
907 let order = builder
908 .build_limit_order(
909 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
910 PolymarketOrderSide::Buy,
911 dec!(0.50),
912 dec!(10),
913 PolymarketOrderType::GTC,
914 "0",
915 false,
916 2,
917 )
918 .unwrap();
919 assert!(
920 timestamps.insert(order.timestamp.clone()),
921 "duplicate timestamp {} in burst",
922 order.timestamp,
923 );
924 }
925 }
926
927 #[rstest]
928 fn test_built_order_carries_nautilus_builder_code() {
929 let builder = make_test_builder();
930 let order = builder
931 .build_limit_order(
932 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
933 PolymarketOrderSide::Buy,
934 dec!(0.50),
935 dec!(10),
936 PolymarketOrderType::GTC,
937 "0",
938 false,
939 2,
940 )
941 .unwrap();
942 assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
943 }
944
945 #[rstest]
946 fn test_poly_1271_order_uses_deposit_wallet_as_signer() {
947 let pk = EvmPrivateKey::new(
948 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
949 )
950 .unwrap();
951 let signer = OrderSigner::new(&pk).unwrap();
952 let signer_address = format!("{:#x}", signer.address());
953 let deposit_wallet = "0x1111111111111111111111111111111111111111".to_string();
954 let builder = PolymarketOrderBuilder::new(
955 signer,
956 signer_address,
957 deposit_wallet.clone(),
958 SignatureType::Poly1271,
959 );
960
961 let order = builder
962 .build_limit_order(
963 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
964 PolymarketOrderSide::Buy,
965 dec!(0.50),
966 dec!(10),
967 PolymarketOrderType::GTC,
968 "0",
969 false,
970 2,
971 )
972 .unwrap();
973
974 assert_eq!(order.maker, deposit_wallet);
975 assert_eq!(order.signer, deposit_wallet);
976 assert_eq!(order.signature_type, SignatureType::Poly1271);
977 assert_eq!(order.signature.len(), 636);
978 }
979
980 #[rstest]
981 fn test_build_limit_order_expiration_passthrough() {
982 let builder = make_test_builder();
983 let order = builder
984 .build_limit_order(
985 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
986 PolymarketOrderSide::Buy,
987 dec!(0.50),
988 dec!(10),
989 PolymarketOrderType::GTD,
990 "1735689600",
991 false,
992 2,
993 )
994 .unwrap();
995 assert_eq!(order.expiration, "1735689600");
996 }
997
998 #[rstest]
999 fn test_build_limit_order_amount_matrix(
1000 #[values(1, 2, 3, 4)] tick_decimals: u32,
1001 #[values(PolymarketOrderSide::Buy, PolymarketOrderSide::Sell)] side: PolymarketOrderSide,
1002 #[values(
1003 PolymarketOrderType::GTC,
1004 PolymarketOrderType::GTD,
1005 PolymarketOrderType::FAK,
1006 PolymarketOrderType::FOK
1007 )]
1008 order_type: PolymarketOrderType,
1009 ) {
1010 let (price, quantity, notional) = match tick_decimals {
1011 1 => (dec!(0.5), dec!(10), dec!(5_000_000)),
1012 2 => (dec!(0.56), dec!(10), dec!(5_600_000)),
1013 3 => (dec!(0.961), dec!(10), dec!(9_610_000)),
1014 4 => (dec!(0.9612), dec!(25), dec!(24_030_000)),
1015 _ => unreachable!(),
1016 };
1017 let quantity_amount = to_fixed_decimal(quantity);
1018 let expiration = if order_type == PolymarketOrderType::GTD {
1019 "1735689600"
1020 } else {
1021 "0"
1022 };
1023 let builder = make_test_builder();
1024
1025 let order = builder
1026 .build_limit_order(
1027 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1028 side,
1029 price,
1030 quantity,
1031 order_type,
1032 expiration,
1033 false,
1034 tick_decimals,
1035 )
1036 .unwrap();
1037
1038 let expected = match side {
1039 PolymarketOrderSide::Buy => (notional, quantity_amount),
1040 PolymarketOrderSide::Sell => (quantity_amount, notional),
1041 };
1042 assert_eq!((order.maker_amount, order.taker_amount), expected);
1043 }
1044
1045 #[rstest]
1046 fn test_build_limit_order_denies_unrepresentable_immediate_buy(
1047 #[values(1, 2, 3, 4)] tick_decimals: u32,
1048 #[values(PolymarketOrderType::FAK, PolymarketOrderType::FOK)]
1049 order_type: PolymarketOrderType,
1050 ) {
1051 let (price, quantity, maker_amount) = match tick_decimals {
1052 1 => (dec!(0.5), dec!(1.01), dec!(0.505)),
1053 2 => (dec!(0.56), dec!(1.01), dec!(0.5656)),
1054 3 => (dec!(0.961), dec!(5), dec!(4.805)),
1055 4 => (dec!(0.9612), dec!(5), dec!(4.806)),
1056 _ => unreachable!(),
1057 };
1058 let builder = make_test_builder();
1059
1060 let error = builder
1061 .build_limit_order(
1062 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1063 PolymarketOrderSide::Buy,
1064 price,
1065 quantity,
1066 order_type,
1067 "0",
1068 false,
1069 tick_decimals,
1070 )
1071 .unwrap_err();
1072
1073 assert_eq!(
1074 error.to_string(),
1075 format!(
1076 "Polymarket {order_type} BUY maker amount {maker_amount} pUSD exceeds 2 decimal places for price {price} and quantity {quantity}"
1077 )
1078 );
1079 }
1080
1081 #[rstest]
1082 #[case::gtc_buy(
1083 PolymarketOrderType::GTC,
1084 PolymarketOrderSide::Buy,
1085 dec!(4_805_000),
1086 dec!(5_000_000),
1087 )]
1088 #[case::gtd_buy(
1089 PolymarketOrderType::GTD,
1090 PolymarketOrderSide::Buy,
1091 dec!(4_805_000),
1092 dec!(5_000_000),
1093 )]
1094 #[case::fak_sell(
1095 PolymarketOrderType::FAK,
1096 PolymarketOrderSide::Sell,
1097 dec!(5_000_000),
1098 dec!(4_805_000),
1099 )]
1100 #[case::fok_sell(
1101 PolymarketOrderType::FOK,
1102 PolymarketOrderSide::Sell,
1103 dec!(5_000_000),
1104 dec!(4_805_000),
1105 )]
1106 fn test_build_limit_order_preserves_unaffected_fractional_cent_amounts(
1107 #[case] order_type: PolymarketOrderType,
1108 #[case] side: PolymarketOrderSide,
1109 #[case] expected_maker: Decimal,
1110 #[case] expected_taker: Decimal,
1111 ) {
1112 let builder = make_test_builder();
1113
1114 let order = builder
1115 .build_limit_order(
1116 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1117 side,
1118 dec!(0.961),
1119 dec!(5),
1120 order_type,
1121 "0",
1122 false,
1123 3,
1124 )
1125 .unwrap();
1126
1127 assert_eq!(order.maker_amount, expected_maker);
1128 assert_eq!(order.taker_amount, expected_taker);
1129 }
1130
1131 #[rstest]
1132 fn test_validate_limit_order_gtd_with_expire_accepted() {
1133 let order = make_limit(false, false, false, TimeInForce::Gtd);
1139 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
1140 }
1141
1142 #[rstest]
1143 fn test_build_market_buy_order_wire_shape() {
1144 let builder = make_test_builder();
1147 let order = builder
1148 .build_market_order(
1149 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1150 PolymarketOrderSide::Buy,
1151 dec!(0.50),
1152 dec!(10),
1153 false,
1154 2,
1155 )
1156 .unwrap();
1157
1158 assert_eq!(order.expiration, "0");
1159 assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
1160 assert_eq!(order.metadata, ZERO_BYTES32);
1161 assert_eq!(order.side, PolymarketOrderSide::Buy);
1162 assert!(!order.timestamp.is_empty());
1163 assert!(!order.signature.is_empty());
1164 assert_eq!(order.maker_amount, dec!(10_000_000));
1167 assert_eq!(order.taker_amount, dec!(20_000_000));
1168 }
1169
1170 #[rstest]
1171 fn test_build_market_buy_order_rejects_fee_adjusted_amount_below_lot_size() {
1172 let builder = make_test_builder();
1173 let adjusted =
1174 adjust_market_buy_amount(dec!(10), dec!(0.009), dec!(0.5), dec!(0.04), 1.0, dec!(0))
1175 .unwrap();
1176 assert_eq!(adjusted, dec!(0.008823));
1177
1178 let err = builder
1179 .build_market_order(
1180 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1181 PolymarketOrderSide::Buy,
1182 dec!(0.5),
1183 adjusted,
1184 false,
1185 2,
1186 )
1187 .unwrap_err();
1188
1189 assert_eq!(
1190 err.to_string(),
1191 "Polymarket market BUY amount 0.008823 pUSD truncates to zero at 2 decimal places",
1192 );
1193 assert_eq!(builder.last_timestamp_ms.load(Ordering::Relaxed), 0);
1194 }
1195
1196 #[rstest]
1197 fn test_build_market_sell_order_wire_shape() {
1198 let builder = make_test_builder();
1201 let order = builder
1202 .build_market_order(
1203 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
1204 PolymarketOrderSide::Sell,
1205 dec!(0.50),
1206 dec!(20),
1207 false,
1208 2,
1209 )
1210 .unwrap();
1211
1212 assert_eq!(order.expiration, "0");
1213 assert_eq!(order.side, PolymarketOrderSide::Sell);
1214 assert_eq!(order.maker_amount, dec!(20_000_000));
1215 assert_eq!(order.taker_amount, dec!(10_000_000));
1216 assert!(!order.signature.is_empty());
1217 }
1218
1219 #[rstest]
1220 fn test_generate_salt_uniqueness() {
1221 let s1 = generate_salt();
1222 let s2 = generate_salt();
1223 assert_ne!(s1, s2);
1224 }
1225
1226 #[rstest]
1227 fn test_generate_salt_within_53_bits() {
1228 for _ in 0..100 {
1229 let s = generate_salt();
1230 assert!(s < (1u64 << 53));
1231 }
1232 }
1233
1234 #[rstest]
1237 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
1238 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
1239 #[case(dec!(0.75), dec!(200), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
1240 #[case(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3, dec!(13_296_150), dec!(23_450_000))]
1242 #[case(dec!(0.55), dec!(10), PolymarketOrderSide::Buy, 1, dec!(5_500_000), dec!(10_000_000))]
1243 fn test_compute_maker_taker_amounts(
1244 #[case] price: Decimal,
1245 #[case] quantity: Decimal,
1246 #[case] side: PolymarketOrderSide,
1247 #[case] tick_decimals: u32,
1248 #[case] expected_maker: Decimal,
1249 #[case] expected_taker: Decimal,
1250 ) {
1251 let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
1252 assert_eq!(maker, expected_maker);
1253 assert_eq!(taker, expected_taker);
1254 }
1255
1256 #[rstest]
1263 #[case::buy_tick_tenth(
1265 dec!(0.5), dec!(21.04), PolymarketOrderSide::Buy, 1,
1266 dec!(10_520_000), dec!(21_040_000),
1267 )]
1268 #[case::sell_tick_tenth(
1269 dec!(0.5), dec!(21.04), PolymarketOrderSide::Sell, 1,
1270 dec!(21_040_000), dec!(10_520_000),
1271 )]
1272 #[case::buy_tick_hundredth(
1274 dec!(0.56), dec!(21.04), PolymarketOrderSide::Buy, 2,
1275 dec!(11_782_400), dec!(21_040_000),
1276 )]
1277 #[case::sell_tick_hundredth(
1278 dec!(0.56), dec!(21.04), PolymarketOrderSide::Sell, 2,
1279 dec!(21_040_000), dec!(11_782_400),
1280 )]
1281 #[case::buy_decimal_accuracy_24(
1282 dec!(0.24), dec!(15), PolymarketOrderSide::Buy, 2,
1283 dec!(3_600_000), dec!(15_000_000),
1284 )]
1285 #[case::buy_decimal_accuracy_82(
1286 dec!(0.82), dec!(101), PolymarketOrderSide::Buy, 2,
1287 dec!(82_820_000), dec!(101_000_000),
1288 )]
1289 #[case::buy_decimal_accuracy_18233(
1290 dec!(0.58), dec!(18233.33), PolymarketOrderSide::Buy, 2,
1291 dec!(10_575_331_400), dec!(18_233_330_000),
1292 )]
1293 #[case::buy_tick_thousandth(
1295 dec!(0.056), dec!(21.04), PolymarketOrderSide::Buy, 3,
1296 dec!(1_178_240), dec!(21_040_000),
1297 )]
1298 #[case::sell_tick_thousandth(
1299 dec!(0.056), dec!(21.04), PolymarketOrderSide::Sell, 3,
1300 dec!(21_040_000), dec!(1_178_240),
1301 )]
1302 #[case::buy_tick_ten_thousandth(
1304 dec!(0.0056), dec!(21.04), PolymarketOrderSide::Buy, 4,
1305 dec!(117_824), dec!(21_040_000),
1306 )]
1307 #[case::sell_tick_ten_thousandth(
1308 dec!(0.0056), dec!(21.04), PolymarketOrderSide::Sell, 4,
1309 dec!(21_040_000), dec!(117_824),
1310 )]
1311 fn test_compute_maker_taker_amounts_sdk_parity(
1312 #[case] price: Decimal,
1313 #[case] quantity: Decimal,
1314 #[case] side: PolymarketOrderSide,
1315 #[case] tick_decimals: u32,
1316 #[case] expected_maker: Decimal,
1317 #[case] expected_taker: Decimal,
1318 ) {
1319 let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
1320 assert_eq!(maker, expected_maker);
1321 assert_eq!(taker, expected_taker);
1322 }
1323
1324 #[rstest]
1327 #[case(dec!(0.50), dec!(50), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
1328 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
1329 #[case(dec!(0.75), dec!(150), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
1330 #[case(dec!(0.96), dec!(5.208), PolymarketOrderSide::Sell, 2, dec!(5_200_000), dec!(4_992_000))]
1331 #[case(dec!(0.211), dec!(23.696681), PolymarketOrderSide::Sell, 3, dec!(23_690_000), dec!(4_998_590))]
1333 fn test_compute_market_maker_taker_amounts(
1334 #[case] price: Decimal,
1335 #[case] amount: Decimal,
1336 #[case] side: PolymarketOrderSide,
1337 #[case] tick_decimals: u32,
1338 #[case] expected_maker: Decimal,
1339 #[case] expected_taker: Decimal,
1340 ) {
1341 let (maker, taker) = compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
1342 assert_eq!(maker, expected_maker);
1343 assert_eq!(taker, expected_taker);
1344 }
1345
1346 #[rstest]
1347 fn test_compute_maker_taker_qty_truncated_to_lot_size() {
1348 let (maker, taker) =
1351 compute_maker_taker_amounts(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3);
1352 assert_eq!(maker, dec!(13_296_150));
1353 assert_eq!(taker, dec!(23_450_000));
1354 }
1355
1356 #[rstest]
1357 fn test_compute_maker_taker_zero_amounts() {
1358 let (maker, taker) =
1359 compute_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
1360 assert_eq!(maker, dec!(0));
1361 assert_eq!(taker, dec!(0));
1362 }
1363
1364 #[rstest]
1365 fn test_compute_maker_taker_tick_decimals_1() {
1366 let (maker, taker) =
1367 compute_maker_taker_amounts(dec!(0.3), dec!(50), PolymarketOrderSide::Buy, 1);
1368 assert_eq!(maker, dec!(15_000_000));
1369 assert_eq!(taker, dec!(50_000_000));
1370 }
1371
1372 #[rstest]
1373 fn test_compute_maker_taker_tick_decimals_3_sell() {
1374 let (maker, taker) =
1377 compute_maker_taker_amounts(dec!(0.789), dec!(15.123), PolymarketOrderSide::Sell, 3);
1378 assert_eq!(maker, dec!(15_120_000));
1379 assert_eq!(taker, dec!(11_929_680));
1380 }
1381
1382 #[rstest]
1383 fn test_compute_market_maker_taker_zero_amount() {
1384 let (maker, taker) =
1385 compute_market_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
1386 assert_eq!(maker, dec!(0));
1387 assert_eq!(taker, dec!(0));
1388 }
1389
1390 #[rstest]
1391 fn test_compute_market_sell_position_close() {
1392 let (maker, taker) = compute_market_maker_taker_amounts(
1396 dec!(0.211),
1397 dec!(23.696681),
1398 PolymarketOrderSide::Sell,
1399 3,
1400 );
1401 assert_eq!(maker, dec!(23_690_000));
1402 assert_eq!(taker, dec!(4_998_590));
1404 assert_eq!(maker % dec!(10_000), dec!(0));
1406 assert_eq!(taker % dec!(10), dec!(0));
1408 }
1409
1410 #[rstest]
1411 fn test_to_fixed_decimal_basic() {
1412 assert_eq!(to_fixed_decimal(dec!(13.29955)), dec!(13_299_550));
1413 assert_eq!(to_fixed_decimal(dec!(100)), dec!(100_000_000));
1414 assert_eq!(to_fixed_decimal(dec!(0)), dec!(0));
1415 }
1416}