1use std::sync::atomic::{AtomicU64, Ordering};
31
32use nautilus_core::time::get_atomic_clock_realtime;
33use nautilus_model::{
34 enums::{OrderSide, OrderType, TimeInForce},
35 identifiers::VenueOrderId,
36 orders::{Order, OrderAny},
37};
38use rust_decimal::Decimal;
39use ustr::Ustr;
40
41use crate::{
42 common::{
43 consts::{LOT_SIZE_SCALE, POLYMARKET_NAUTILUS_BUILDER_CODE, USDC_DECIMALS},
44 enums::{PolymarketOrderSide, PolymarketOrderType, SignatureType},
45 },
46 http::models::PolymarketOrder,
47 signing::eip712::{OrderSigner, order_hash},
48};
49
50pub const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
52
53#[derive(Debug)]
59pub struct PolymarketOrderBuilder {
60 order_signer: OrderSigner,
61 signer_address: String,
62 maker_address: String,
63 signature_type: SignatureType,
64 last_timestamp_ms: AtomicU64,
65}
66
67impl PolymarketOrderBuilder {
68 pub fn new(
70 order_signer: OrderSigner,
71 signer_address: String,
72 maker_address: String,
73 signature_type: SignatureType,
74 ) -> Self {
75 Self {
76 order_signer,
77 signer_address,
78 maker_address,
79 signature_type,
80 last_timestamp_ms: AtomicU64::new(0),
81 }
82 }
83
84 fn next_timestamp_ms(&self) -> u64 {
87 let now_ms = get_atomic_clock_realtime().get_time_ns().as_u64() / 1_000_000;
88
89 loop {
90 let prev = self.last_timestamp_ms.load(Ordering::Relaxed);
91 let candidate = prev.saturating_add(1).max(now_ms);
92
93 if self
94 .last_timestamp_ms
95 .compare_exchange_weak(prev, candidate, Ordering::Relaxed, Ordering::Relaxed)
96 .is_ok()
97 {
98 return candidate;
99 }
100 }
101 }
102
103 #[expect(clippy::too_many_arguments)]
108 pub fn build_limit_order(
109 &self,
110 token_id: &str,
111 side: PolymarketOrderSide,
112 price: Decimal,
113 quantity: Decimal,
114 expiration: &str,
115 neg_risk: bool,
116 tick_decimals: u32,
117 ) -> anyhow::Result<PolymarketOrder> {
118 let (maker_amount, taker_amount) =
119 compute_maker_taker_amounts(price, quantity, side, tick_decimals);
120 self.build_and_sign(
121 token_id,
122 side,
123 maker_amount,
124 taker_amount,
125 expiration,
126 neg_risk,
127 )
128 }
129
130 pub fn build_market_order(
138 &self,
139 token_id: &str,
140 side: PolymarketOrderSide,
141 price: Decimal,
142 amount: Decimal,
143 neg_risk: bool,
144 tick_decimals: u32,
145 ) -> anyhow::Result<PolymarketOrder> {
146 let (maker_amount, taker_amount) =
147 compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
148 self.build_and_sign(token_id, side, maker_amount, taker_amount, "0", neg_risk)
149 }
150
151 pub fn expected_order_id(
153 &self,
154 order: &PolymarketOrder,
155 neg_risk: bool,
156 ) -> anyhow::Result<VenueOrderId> {
157 let hash = order_hash(order, neg_risk)
158 .map_err(|e| anyhow::anyhow!("Failed to derive order hash: {e}"))?;
159 let order_id = format!("{hash:#x}");
160 Ok(VenueOrderId::from(order_id.as_str()))
161 }
162
163 pub fn validate_limit_order(order: &OrderAny) -> Result<(), String> {
165 if order.is_reduce_only() {
166 return Err("Reduce-only orders not supported on Polymarket".to_string());
167 }
168
169 if order.order_type() != OrderType::Limit {
170 return Err(format!(
171 "Unsupported order type for Polymarket: {:?}",
172 order.order_type()
173 ));
174 }
175
176 if order.is_quote_quantity() {
177 return Err("Quote quantity not supported for limit orders".to_string());
178 }
179
180 if order.price().is_none() {
181 return Err("Limit orders require a price".to_string());
182 }
183
184 if PolymarketOrderType::try_from(order.time_in_force()).is_err() {
185 return Err(format!(
186 "Unsupported time in force: {:?}",
187 order.time_in_force()
188 ));
189 }
190
191 if PolymarketOrderSide::try_from(order.order_side()).is_err() {
192 return Err(format!("Invalid order side: {:?}", order.order_side()));
193 }
194
195 if order.is_post_only()
196 && !matches!(order.time_in_force(), TimeInForce::Gtc | TimeInForce::Gtd)
197 {
198 return Err("Post-only orders require GTC or GTD time in force".to_string());
199 }
200
201 Ok(())
202 }
203
204 pub fn validate_market_order(order: &OrderAny) -> Result<(), String> {
206 if order.is_reduce_only() {
207 return Err("Reduce-only orders not supported on Polymarket".to_string());
208 }
209
210 if order.order_type() != OrderType::Market {
211 return Err(format!(
212 "Expected Market order, was {:?}",
213 order.order_type()
214 ));
215 }
216
217 match order.order_side() {
220 OrderSide::Buy => {
221 if !order.is_quote_quantity() {
222 return Err(
223 "Market BUY orders require quote_quantity=true (amount in pUSD)"
224 .to_string(),
225 );
226 }
227 }
228 OrderSide::Sell => {
229 if order.is_quote_quantity() {
230 return Err(
231 "Market SELL orders require quote_quantity=false (amount in shares)"
232 .to_string(),
233 );
234 }
235 }
236 _ => {
237 return Err(format!("Invalid order side: {:?}", order.order_side()));
238 }
239 }
240
241 if PolymarketOrderType::from_market_time_in_force(order.time_in_force()).is_err() {
242 return Err(format!(
243 "Unsupported time in force for Polymarket market order: {:?}; use IOC or FOK",
244 order.time_in_force()
245 ));
246 }
247
248 Ok(())
249 }
250
251 fn build_and_sign(
252 &self,
253 token_id: &str,
254 side: PolymarketOrderSide,
255 maker_amount: Decimal,
256 taker_amount: Decimal,
257 expiration: &str,
258 neg_risk: bool,
259 ) -> anyhow::Result<PolymarketOrder> {
260 let salt = generate_salt();
261 let timestamp_ms = self.next_timestamp_ms();
262
263 let mut poly_order = PolymarketOrder {
264 salt,
265 maker: self.maker_address.clone(),
266 signer: self.order_signer_address(),
267 token_id: Ustr::from(token_id),
268 maker_amount,
269 taker_amount,
270 side,
271 signature_type: self.signature_type,
272 expiration: expiration.to_string(),
273 timestamp: timestamp_ms.to_string(),
274 metadata: ZERO_BYTES32.to_string(),
275 builder: POLYMARKET_NAUTILUS_BUILDER_CODE.to_string(),
276 signature: String::new(),
277 };
278
279 let signature = self
280 .order_signer
281 .sign_order(&poly_order, neg_risk)
282 .map_err(|e| anyhow::anyhow!("EIP-712 signing failed: {e}"))?;
283 poly_order.signature = signature;
284
285 Ok(poly_order)
286 }
287
288 fn order_signer_address(&self) -> String {
289 match self.signature_type {
290 SignatureType::Poly1271 => self.maker_address.clone(),
291 _ => self.signer_address.clone(),
292 }
293 }
294}
295
296fn to_fixed_decimal(d: Decimal) -> Decimal {
297 let mantissa = d.normalize().trunc_with_scale(USDC_DECIMALS).mantissa();
298 Decimal::from(mantissa)
299}
300
301pub fn compute_maker_taker_amounts(
310 price: Decimal,
311 quantity: Decimal,
312 side: PolymarketOrderSide,
313 tick_decimals: u32,
314) -> (Decimal, Decimal) {
315 let precision = tick_decimals + LOT_SIZE_SCALE;
316 let qty = quantity.trunc_with_scale(LOT_SIZE_SCALE);
317
318 match side {
319 PolymarketOrderSide::Buy => {
320 let maker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
321 let taker_amount = to_fixed_decimal(qty);
322 (maker_amount, taker_amount)
323 }
324 PolymarketOrderSide::Sell => {
325 let maker_amount = to_fixed_decimal(qty);
326 let taker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
327 (maker_amount, taker_amount)
328 }
329 }
330}
331
332pub fn compute_market_maker_taker_amounts(
342 price: Decimal,
343 amount: Decimal,
344 side: PolymarketOrderSide,
345 tick_decimals: u32,
346) -> (Decimal, Decimal) {
347 let precision = tick_decimals + LOT_SIZE_SCALE;
348 let amt = amount.trunc_with_scale(LOT_SIZE_SCALE);
349
350 match side {
351 PolymarketOrderSide::Buy => {
352 let maker_amount = to_fixed_decimal(amt);
353 let taker_amount = to_fixed_decimal((amt / price).trunc_with_scale(precision));
354 (maker_amount, taker_amount)
355 }
356 PolymarketOrderSide::Sell => {
357 let maker_amount = to_fixed_decimal(amt);
358 let taker_amount = to_fixed_decimal((amt * price).trunc_with_scale(precision));
359 (maker_amount, taker_amount)
360 }
361 }
362}
363
364pub fn generate_salt() -> u64 {
370 let bytes = uuid::Uuid::new_v4().into_bytes();
371 u64::from_le_bytes(bytes[..8].try_into().unwrap()) & ((1u64 << 53) - 1)
372}
373
374#[cfg(test)]
375mod tests {
376 use nautilus_core::{UUID4, UnixNanos};
377 use nautilus_model::{
378 enums::{OrderSide, TimeInForce},
379 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
380 orders::{LimitOrder, MarketOrder, OrderAny},
381 types::{Price, Quantity},
382 };
383 use rstest::rstest;
384 use rust_decimal::Decimal;
385 use rust_decimal_macros::dec;
386
387 use super::*;
388 use crate::{
389 common::{credential::EvmPrivateKey, enums::PolymarketOrderSide},
390 signing::eip712::OrderSigner,
391 };
392
393 fn make_limit(
394 reduce_only: bool,
395 quote_quantity: bool,
396 post_only: bool,
397 tif: TimeInForce,
398 ) -> OrderAny {
399 let expire_time = if tif == TimeInForce::Gtd {
400 Some(UnixNanos::from(2_000_000_000_000_000_000u64))
401 } else {
402 None
403 };
404 OrderAny::Limit(LimitOrder::new(
405 TraderId::from("TESTER-001"),
406 StrategyId::from("S-001"),
407 InstrumentId::from("TEST.POLYMARKET"),
408 ClientOrderId::from("O-001"),
409 OrderSide::Buy,
410 Quantity::from("10"),
411 Price::from("0.50"),
412 tif,
413 expire_time,
414 post_only,
415 reduce_only,
416 quote_quantity,
417 None,
418 None,
419 None,
420 None,
421 None,
422 None,
423 None,
424 None,
425 None,
426 None,
427 None,
428 UUID4::new(),
429 UnixNanos::default(),
430 ))
431 }
432
433 fn make_market(side: OrderSide, quote_quantity: bool) -> OrderAny {
434 OrderAny::Market(MarketOrder::new(
435 TraderId::from("TESTER-001"),
436 StrategyId::from("S-001"),
437 InstrumentId::from("TEST.POLYMARKET"),
438 ClientOrderId::from("O-001"),
439 side,
440 Quantity::from("10"),
441 TimeInForce::Ioc,
442 UUID4::new(),
443 UnixNanos::default(),
444 false,
445 quote_quantity,
446 None,
447 None,
448 None,
449 None,
450 None,
451 None,
452 None,
453 None,
454 ))
455 }
456
457 #[rstest]
458 fn test_validate_limit_order_valid() {
459 let order = make_limit(false, false, false, TimeInForce::Gtc);
460 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
461 }
462
463 #[rstest]
464 fn test_validate_limit_order_reduce_only_denied() {
465 let order = make_limit(true, false, false, TimeInForce::Gtc);
466 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
467 assert!(err.contains("Reduce-only"));
468 }
469
470 #[rstest]
471 fn test_validate_limit_order_quote_quantity_denied() {
472 let order = make_limit(false, true, false, TimeInForce::Gtc);
473 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
474 assert!(err.contains("Quote quantity"));
475 }
476
477 #[rstest]
478 fn test_validate_limit_order_post_only_ioc_denied() {
479 let order = make_limit(false, false, true, TimeInForce::Ioc);
480 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
481 assert!(err.contains("Post-only"));
482 }
483
484 #[rstest]
485 fn test_validate_limit_order_post_only_gtc_allowed() {
486 let order = make_limit(false, false, true, TimeInForce::Gtc);
487 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
488 }
489
490 #[rstest]
491 fn test_validate_limit_order_no_order_side_denied() {
492 let order = OrderAny::Limit(LimitOrder::new(
493 TraderId::from("TESTER-001"),
494 StrategyId::from("S-001"),
495 InstrumentId::from("TEST.POLYMARKET"),
496 ClientOrderId::from("O-NO-SIDE"),
497 OrderSide::NoOrderSide,
498 Quantity::from("10"),
499 Price::from("0.50"),
500 TimeInForce::Gtc,
501 None,
502 false,
503 false,
504 false,
505 None,
506 None,
507 None,
508 None,
509 None,
510 None,
511 None,
512 None,
513 None,
514 None,
515 None,
516 UUID4::new(),
517 UnixNanos::default(),
518 ));
519 let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
520 assert!(err.contains("Invalid order side"));
521 }
522
523 #[rstest]
524 fn test_validate_market_order_buy_with_quote_qty() {
525 let order = make_market(OrderSide::Buy, true);
526 assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
527 }
528
529 #[rstest]
530 fn test_validate_market_order_buy_without_quote_qty_denied() {
531 let order = make_market(OrderSide::Buy, false);
532 let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
533 assert!(err.contains("quote_quantity=true"));
534 }
535
536 #[rstest]
537 fn test_validate_market_order_sell_without_quote_qty() {
538 let order = make_market(OrderSide::Sell, false);
539 assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
540 }
541
542 #[rstest]
543 fn test_validate_market_order_sell_with_quote_qty_denied() {
544 let order = make_market(OrderSide::Sell, true);
545 let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
546 assert!(err.contains("quote_quantity=false"));
547 }
548
549 #[rstest]
550 fn test_validate_market_order_wrong_type_denied() {
551 let limit = make_limit(false, false, false, TimeInForce::Gtc);
553 let err = PolymarketOrderBuilder::validate_market_order(&limit).unwrap_err();
554 assert!(err.contains("Expected Market order"));
555 }
556
557 fn make_test_builder() -> PolymarketOrderBuilder {
558 let pk = EvmPrivateKey::new(
559 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
560 )
561 .unwrap();
562 let signer = OrderSigner::new(&pk).unwrap();
563 let addr = format!("{:#x}", signer.address());
564 PolymarketOrderBuilder::new(signer, addr.clone(), addr, SignatureType::Eoa)
565 }
566
567 #[rstest]
568 fn test_next_timestamp_ms_is_strictly_monotonic() {
569 let builder = make_test_builder();
570 let mut prev = builder.next_timestamp_ms();
571 for _ in 0..1_000 {
572 let next = builder.next_timestamp_ms();
573 assert!(
574 next > prev,
575 "timestamp not strictly monotonic: {prev} >= {next}"
576 );
577 prev = next;
578 }
579 }
580
581 #[rstest]
582 fn test_build_orders_produce_unique_timestamps() {
583 let builder = make_test_builder();
584 let mut timestamps = ahash::AHashSet::new();
585
586 for _ in 0..50 {
587 let order = builder
588 .build_limit_order(
589 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
590 PolymarketOrderSide::Buy,
591 dec!(0.50),
592 dec!(10),
593 "0",
594 false,
595 2,
596 )
597 .unwrap();
598 assert!(
599 timestamps.insert(order.timestamp.clone()),
600 "duplicate timestamp {} in burst",
601 order.timestamp,
602 );
603 }
604 }
605
606 #[rstest]
607 fn test_built_order_carries_nautilus_builder_code() {
608 let builder = make_test_builder();
609 let order = builder
610 .build_limit_order(
611 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
612 PolymarketOrderSide::Buy,
613 dec!(0.50),
614 dec!(10),
615 "0",
616 false,
617 2,
618 )
619 .unwrap();
620 assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
621 }
622
623 #[rstest]
624 fn test_poly_1271_order_uses_deposit_wallet_as_signer() {
625 let pk = EvmPrivateKey::new(
626 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
627 )
628 .unwrap();
629 let signer = OrderSigner::new(&pk).unwrap();
630 let signer_address = format!("{:#x}", signer.address());
631 let deposit_wallet = "0x1111111111111111111111111111111111111111".to_string();
632 let builder = PolymarketOrderBuilder::new(
633 signer,
634 signer_address,
635 deposit_wallet.clone(),
636 SignatureType::Poly1271,
637 );
638
639 let order = builder
640 .build_limit_order(
641 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
642 PolymarketOrderSide::Buy,
643 dec!(0.50),
644 dec!(10),
645 "0",
646 false,
647 2,
648 )
649 .unwrap();
650
651 assert_eq!(order.maker, deposit_wallet);
652 assert_eq!(order.signer, deposit_wallet);
653 assert_eq!(order.signature_type, SignatureType::Poly1271);
654 assert_eq!(order.signature.len(), 636);
655 }
656
657 #[rstest]
658 fn test_build_limit_order_expiration_passthrough() {
659 let builder = make_test_builder();
660 let order = builder
661 .build_limit_order(
662 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
663 PolymarketOrderSide::Buy,
664 dec!(0.50),
665 dec!(10),
666 "1735689600",
667 false,
668 2,
669 )
670 .unwrap();
671 assert_eq!(order.expiration, "1735689600");
672 }
673
674 #[rstest]
675 fn test_validate_limit_order_gtd_with_expire_accepted() {
676 let order = make_limit(false, false, false, TimeInForce::Gtd);
682 assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
683 }
684
685 #[rstest]
686 fn test_build_market_buy_order_wire_shape() {
687 let builder = make_test_builder();
690 let order = builder
691 .build_market_order(
692 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
693 PolymarketOrderSide::Buy,
694 dec!(0.50),
695 dec!(10),
696 false,
697 2,
698 )
699 .unwrap();
700
701 assert_eq!(order.expiration, "0");
702 assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
703 assert_eq!(order.metadata, ZERO_BYTES32);
704 assert_eq!(order.side, PolymarketOrderSide::Buy);
705 assert!(!order.timestamp.is_empty());
706 assert!(!order.signature.is_empty());
707 assert_eq!(order.maker_amount, dec!(10_000_000));
710 assert_eq!(order.taker_amount, dec!(20_000_000));
711 }
712
713 #[rstest]
714 fn test_build_market_sell_order_wire_shape() {
715 let builder = make_test_builder();
718 let order = builder
719 .build_market_order(
720 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
721 PolymarketOrderSide::Sell,
722 dec!(0.50),
723 dec!(20),
724 false,
725 2,
726 )
727 .unwrap();
728
729 assert_eq!(order.expiration, "0");
730 assert_eq!(order.side, PolymarketOrderSide::Sell);
731 assert_eq!(order.maker_amount, dec!(20_000_000));
732 assert_eq!(order.taker_amount, dec!(10_000_000));
733 assert!(!order.signature.is_empty());
734 }
735
736 #[rstest]
737 fn test_generate_salt_uniqueness() {
738 let s1 = generate_salt();
739 let s2 = generate_salt();
740 assert_ne!(s1, s2);
741 }
742
743 #[rstest]
744 fn test_generate_salt_within_53_bits() {
745 for _ in 0..100 {
746 let s = generate_salt();
747 assert!(s < (1u64 << 53));
748 }
749 }
750
751 #[rstest]
754 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
755 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
756 #[case(dec!(0.75), dec!(200), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
757 #[case(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3, dec!(13_296_150), dec!(23_450_000))]
759 #[case(dec!(0.55), dec!(10), PolymarketOrderSide::Buy, 1, dec!(5_500_000), dec!(10_000_000))]
760 fn test_compute_maker_taker_amounts(
761 #[case] price: Decimal,
762 #[case] quantity: Decimal,
763 #[case] side: PolymarketOrderSide,
764 #[case] tick_decimals: u32,
765 #[case] expected_maker: Decimal,
766 #[case] expected_taker: Decimal,
767 ) {
768 let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
769 assert_eq!(maker, expected_maker);
770 assert_eq!(taker, expected_taker);
771 }
772
773 #[rstest]
780 #[case::buy_tick_tenth(
782 dec!(0.5), dec!(21.04), PolymarketOrderSide::Buy, 1,
783 dec!(10_520_000), dec!(21_040_000),
784 )]
785 #[case::sell_tick_tenth(
786 dec!(0.5), dec!(21.04), PolymarketOrderSide::Sell, 1,
787 dec!(21_040_000), dec!(10_520_000),
788 )]
789 #[case::buy_tick_hundredth(
791 dec!(0.56), dec!(21.04), PolymarketOrderSide::Buy, 2,
792 dec!(11_782_400), dec!(21_040_000),
793 )]
794 #[case::sell_tick_hundredth(
795 dec!(0.56), dec!(21.04), PolymarketOrderSide::Sell, 2,
796 dec!(21_040_000), dec!(11_782_400),
797 )]
798 #[case::buy_decimal_accuracy_24(
799 dec!(0.24), dec!(15), PolymarketOrderSide::Buy, 2,
800 dec!(3_600_000), dec!(15_000_000),
801 )]
802 #[case::buy_decimal_accuracy_82(
803 dec!(0.82), dec!(101), PolymarketOrderSide::Buy, 2,
804 dec!(82_820_000), dec!(101_000_000),
805 )]
806 #[case::buy_decimal_accuracy_18233(
807 dec!(0.58), dec!(18233.33), PolymarketOrderSide::Buy, 2,
808 dec!(10_575_331_400), dec!(18_233_330_000),
809 )]
810 #[case::buy_tick_thousandth(
812 dec!(0.056), dec!(21.04), PolymarketOrderSide::Buy, 3,
813 dec!(1_178_240), dec!(21_040_000),
814 )]
815 #[case::sell_tick_thousandth(
816 dec!(0.056), dec!(21.04), PolymarketOrderSide::Sell, 3,
817 dec!(21_040_000), dec!(1_178_240),
818 )]
819 #[case::buy_tick_ten_thousandth(
821 dec!(0.0056), dec!(21.04), PolymarketOrderSide::Buy, 4,
822 dec!(117_824), dec!(21_040_000),
823 )]
824 #[case::sell_tick_ten_thousandth(
825 dec!(0.0056), dec!(21.04), PolymarketOrderSide::Sell, 4,
826 dec!(21_040_000), dec!(117_824),
827 )]
828 fn test_compute_maker_taker_amounts_sdk_parity(
829 #[case] price: Decimal,
830 #[case] quantity: Decimal,
831 #[case] side: PolymarketOrderSide,
832 #[case] tick_decimals: u32,
833 #[case] expected_maker: Decimal,
834 #[case] expected_taker: Decimal,
835 ) {
836 let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
837 assert_eq!(maker, expected_maker);
838 assert_eq!(taker, expected_taker);
839 }
840
841 #[rstest]
844 #[case(dec!(0.50), dec!(50), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
845 #[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
846 #[case(dec!(0.75), dec!(150), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
847 #[case(dec!(0.211), dec!(23.696681), PolymarketOrderSide::Sell, 3, dec!(23_690_000), dec!(4_998_590))]
849 fn test_compute_market_maker_taker_amounts(
850 #[case] price: Decimal,
851 #[case] amount: Decimal,
852 #[case] side: PolymarketOrderSide,
853 #[case] tick_decimals: u32,
854 #[case] expected_maker: Decimal,
855 #[case] expected_taker: Decimal,
856 ) {
857 let (maker, taker) = compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
858 assert_eq!(maker, expected_maker);
859 assert_eq!(taker, expected_taker);
860 }
861
862 #[rstest]
863 fn test_compute_maker_taker_qty_truncated_to_lot_size() {
864 let (maker, taker) =
867 compute_maker_taker_amounts(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3);
868 assert_eq!(maker, dec!(13_296_150));
869 assert_eq!(taker, dec!(23_450_000));
870 }
871
872 #[rstest]
873 fn test_compute_maker_taker_zero_amounts() {
874 let (maker, taker) =
875 compute_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
876 assert_eq!(maker, dec!(0));
877 assert_eq!(taker, dec!(0));
878 }
879
880 #[rstest]
881 fn test_compute_maker_taker_tick_decimals_1() {
882 let (maker, taker) =
883 compute_maker_taker_amounts(dec!(0.3), dec!(50), PolymarketOrderSide::Buy, 1);
884 assert_eq!(maker, dec!(15_000_000));
885 assert_eq!(taker, dec!(50_000_000));
886 }
887
888 #[rstest]
889 fn test_compute_maker_taker_tick_decimals_3_sell() {
890 let (maker, taker) =
893 compute_maker_taker_amounts(dec!(0.789), dec!(15.123), PolymarketOrderSide::Sell, 3);
894 assert_eq!(maker, dec!(15_120_000));
895 assert_eq!(taker, dec!(11_929_680));
896 }
897
898 #[rstest]
899 fn test_compute_market_maker_taker_zero_amount() {
900 let (maker, taker) =
901 compute_market_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
902 assert_eq!(maker, dec!(0));
903 assert_eq!(taker, dec!(0));
904 }
905
906 #[rstest]
907 fn test_compute_market_sell_position_close() {
908 let (maker, taker) = compute_market_maker_taker_amounts(
912 dec!(0.211),
913 dec!(23.696681),
914 PolymarketOrderSide::Sell,
915 3,
916 );
917 assert_eq!(maker, dec!(23_690_000));
918 assert_eq!(taker, dec!(4_998_590));
920 assert_eq!(maker % dec!(10_000), dec!(0));
922 assert_eq!(taker % dec!(10), dec!(0));
924 }
925
926 #[rstest]
927 fn test_to_fixed_decimal_basic() {
928 assert_eq!(to_fixed_decimal(dec!(13.29955)), dec!(13_299_550));
929 assert_eq!(to_fixed_decimal(dec!(100)), dec!(100_000_000));
930 assert_eq!(to_fixed_decimal(dec!(0)), dec!(0));
931 }
932}