use std::sync::atomic::{AtomicU64, Ordering};
use nautilus_core::time::get_atomic_clock_realtime;
use nautilus_model::{
enums::{OrderSide, OrderType, TimeInForce},
identifiers::VenueOrderId,
orders::{Order, OrderAny},
types::Price,
};
use rust_decimal::Decimal;
use ustr::Ustr;
use crate::{
common::{
consts::{LOT_SIZE_SCALE, POLYMARKET_NAUTILUS_BUILDER_CODE, USDC_DECIMALS},
enums::{PolymarketOrderSide, PolymarketOrderType, SignatureType},
},
http::models::PolymarketOrder,
signing::eip712::{OrderSigner, order_hash},
};
pub const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
#[derive(Debug)]
pub struct PolymarketOrderBuilder {
order_signer: OrderSigner,
signer_address: String,
maker_address: String,
signature_type: SignatureType,
last_timestamp_ms: AtomicU64,
}
impl PolymarketOrderBuilder {
pub fn new(
order_signer: OrderSigner,
signer_address: String,
maker_address: String,
signature_type: SignatureType,
) -> Self {
Self {
order_signer,
signer_address,
maker_address,
signature_type,
last_timestamp_ms: AtomicU64::new(0),
}
}
fn next_timestamp_ms(&self) -> u64 {
let now_ms = get_atomic_clock_realtime().get_time_ns().as_u64() / 1_000_000;
loop {
let prev = self.last_timestamp_ms.load(Ordering::Relaxed);
let candidate = prev.saturating_add(1).max(now_ms);
if self
.last_timestamp_ms
.compare_exchange_weak(prev, candidate, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
return candidate;
}
}
}
#[expect(clippy::too_many_arguments)]
pub fn build_limit_order(
&self,
token_id: &str,
side: PolymarketOrderSide,
price: Decimal,
quantity: Decimal,
order_type: PolymarketOrderType,
expiration: &str,
neg_risk: bool,
tick_decimals: u32,
) -> anyhow::Result<PolymarketOrder> {
validate_immediate_buy_maker_amount(price, quantity, side, order_type)
.map_err(anyhow::Error::msg)?;
let (maker_amount, taker_amount) =
compute_maker_taker_amounts(price, quantity, side, tick_decimals);
self.build_and_sign(
token_id,
side,
maker_amount,
taker_amount,
expiration,
neg_risk,
)
}
pub fn build_market_order(
&self,
token_id: &str,
side: PolymarketOrderSide,
price: Decimal,
amount: Decimal,
neg_risk: bool,
tick_decimals: u32,
) -> anyhow::Result<PolymarketOrder> {
let (maker_amount, taker_amount) =
compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
self.build_and_sign(token_id, side, maker_amount, taker_amount, "0", neg_risk)
}
pub fn expected_order_id(
&self,
order: &PolymarketOrder,
neg_risk: bool,
) -> anyhow::Result<VenueOrderId> {
let hash = order_hash(order, neg_risk)
.map_err(|e| anyhow::anyhow!("Failed to derive order hash: {e}"))?;
let order_id = format!("{hash:#x}");
Ok(VenueOrderId::from(order_id.as_str()))
}
pub fn validate_limit_order(order: &OrderAny) -> Result<(), String> {
if order.is_reduce_only() {
return Err("Reduce-only orders not supported on Polymarket".to_string());
}
if order.order_type() != OrderType::Limit {
return Err(format!(
"Unsupported order type for Polymarket: {:?}",
order.order_type()
));
}
if order.is_quote_quantity() {
return Err("Quote quantity not supported for limit orders".to_string());
}
let price = order
.price()
.ok_or_else(|| "Limit orders require a price".to_string())?;
let order_type = PolymarketOrderType::try_from(order.time_in_force())
.map_err(|_| format!("Unsupported time in force: {:?}", order.time_in_force()))?;
let side = PolymarketOrderSide::try_from(order.order_side())
.map_err(|_| format!("Invalid order side: {:?}", order.order_side()))?;
if order.is_post_only()
&& !matches!(order.time_in_force(), TimeInForce::Gtc | TimeInForce::Gtd)
{
return Err("Post-only orders require GTC or GTD time in force".to_string());
}
validate_immediate_buy_maker_amount(
price.as_decimal(),
order.quantity().as_decimal(),
side,
order_type,
)?;
Ok(())
}
pub(crate) fn validate_limit_price(order: &OrderAny, tick_size: Price) -> Result<(), String> {
let price = order
.price()
.ok_or_else(|| "Limit orders require a price".to_string())?;
let tick = tick_size.as_decimal();
let max_price = Decimal::ONE - tick;
let price_decimal = price.as_decimal();
if price_decimal < tick || price_decimal > max_price {
return Err(format!(
"Limit order price {price} outside Polymarket range [{tick}, {max_price}]"
));
}
if price_decimal % tick != Decimal::ZERO {
return Err(format!(
"Limit order price {price} does not conform to Polymarket tick size {tick}"
));
}
Ok(())
}
pub fn validate_market_order(order: &OrderAny) -> Result<(), String> {
if order.is_reduce_only() {
return Err("Reduce-only orders not supported on Polymarket".to_string());
}
if order.order_type() != OrderType::Market {
return Err(format!(
"Expected Market order, was {:?}",
order.order_type()
));
}
match order.order_side() {
OrderSide::Buy => {
if !order.is_quote_quantity() {
return Err(
"Market BUY orders require quote_quantity=true (amount in pUSD)"
.to_string(),
);
}
}
OrderSide::Sell => {
if order.is_quote_quantity() {
return Err(
"Market SELL orders require quote_quantity=false (amount in shares)"
.to_string(),
);
}
}
_ => {
return Err(format!("Invalid order side: {:?}", order.order_side()));
}
}
if PolymarketOrderType::from_market_time_in_force(order.time_in_force()).is_err() {
return Err(format!(
"Unsupported time in force for Polymarket market order: {:?}; use IOC or FOK",
order.time_in_force()
));
}
Ok(())
}
fn build_and_sign(
&self,
token_id: &str,
side: PolymarketOrderSide,
maker_amount: Decimal,
taker_amount: Decimal,
expiration: &str,
neg_risk: bool,
) -> anyhow::Result<PolymarketOrder> {
let salt = generate_salt();
let timestamp_ms = self.next_timestamp_ms();
let mut poly_order = PolymarketOrder {
salt,
maker: self.maker_address.clone(),
signer: self.order_signer_address(),
token_id: Ustr::from(token_id),
maker_amount,
taker_amount,
side,
signature_type: self.signature_type,
expiration: expiration.to_string(),
timestamp: timestamp_ms.to_string(),
metadata: ZERO_BYTES32.to_string(),
builder: POLYMARKET_NAUTILUS_BUILDER_CODE.to_string(),
signature: String::new(),
};
let signature = self
.order_signer
.sign_order(&poly_order, neg_risk)
.map_err(|e| anyhow::anyhow!("EIP-712 signing failed: {e}"))?;
poly_order.signature = signature;
Ok(poly_order)
}
fn order_signer_address(&self) -> String {
match self.signature_type {
SignatureType::Poly1271 => self.maker_address.clone(),
_ => self.signer_address.clone(),
}
}
}
fn to_fixed_decimal(d: Decimal) -> Decimal {
let mantissa = d.normalize().trunc_with_scale(USDC_DECIMALS).mantissa();
Decimal::from(mantissa)
}
fn validate_immediate_buy_maker_amount(
price: Decimal,
quantity: Decimal,
side: PolymarketOrderSide,
order_type: PolymarketOrderType,
) -> Result<(), String> {
if side != PolymarketOrderSide::Buy
|| !matches!(
order_type,
PolymarketOrderType::FAK | PolymarketOrderType::FOK
)
{
return Ok(());
}
let quantity = quantity.trunc_with_scale(LOT_SIZE_SCALE);
let maker_amount = (quantity * price).normalize();
if maker_amount.scale() > LOT_SIZE_SCALE {
return Err(format!(
"Polymarket {order_type} BUY maker amount {} pUSD exceeds 2 decimal places for price {} and quantity {}",
maker_amount,
price.normalize(),
quantity.normalize(),
));
}
Ok(())
}
pub fn compute_maker_taker_amounts(
price: Decimal,
quantity: Decimal,
side: PolymarketOrderSide,
tick_decimals: u32,
) -> (Decimal, Decimal) {
let precision = tick_decimals + LOT_SIZE_SCALE;
let qty = quantity.trunc_with_scale(LOT_SIZE_SCALE);
match side {
PolymarketOrderSide::Buy => {
let maker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
let taker_amount = to_fixed_decimal(qty);
(maker_amount, taker_amount)
}
PolymarketOrderSide::Sell => {
let maker_amount = to_fixed_decimal(qty);
let taker_amount = to_fixed_decimal((qty * price).trunc_with_scale(precision));
(maker_amount, taker_amount)
}
}
}
pub fn compute_market_maker_taker_amounts(
price: Decimal,
amount: Decimal,
side: PolymarketOrderSide,
tick_decimals: u32,
) -> (Decimal, Decimal) {
let precision = tick_decimals + LOT_SIZE_SCALE;
let amt = amount.trunc_with_scale(LOT_SIZE_SCALE);
match side {
PolymarketOrderSide::Buy => {
let maker_amount = to_fixed_decimal(amt);
let taker_amount = to_fixed_decimal((amt / price).trunc_with_scale(precision));
(maker_amount, taker_amount)
}
PolymarketOrderSide::Sell => {
let maker_amount = to_fixed_decimal(amt);
let taker_amount = to_fixed_decimal((amt * price).trunc_with_scale(precision));
(maker_amount, taker_amount)
}
}
}
pub fn generate_salt() -> u64 {
let bytes = uuid::Uuid::new_v4().into_bytes();
u64::from_le_bytes(bytes[..8].try_into().unwrap()) & ((1u64 << 53) - 1)
}
#[cfg(test)]
mod tests {
use nautilus_core::{UUID4, UnixNanos};
use nautilus_model::{
enums::{OrderSide, TimeInForce},
identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
orders::{LimitOrder, MarketOrder, OrderAny},
types::{Price, Quantity},
};
use rstest::rstest;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use super::*;
use crate::{
common::{credential::EvmPrivateKey, enums::PolymarketOrderSide},
signing::eip712::OrderSigner,
};
fn make_limit(
reduce_only: bool,
quote_quantity: bool,
post_only: bool,
tif: TimeInForce,
) -> OrderAny {
make_limit_at_price(
reduce_only,
quote_quantity,
post_only,
tif,
Price::from("0.50"),
)
}
fn make_limit_at_price(
reduce_only: bool,
quote_quantity: bool,
post_only: bool,
tif: TimeInForce,
price: Price,
) -> OrderAny {
let expire_time = if tif == TimeInForce::Gtd {
Some(UnixNanos::from(2_000_000_000_000_000_000u64))
} else {
None
};
OrderAny::Limit(LimitOrder::new(
TraderId::from("TESTER-001"),
StrategyId::from("S-001"),
InstrumentId::from("TEST.POLYMARKET"),
ClientOrderId::from("O-001"),
OrderSide::Buy,
Quantity::from("10"),
price,
tif,
expire_time,
post_only,
reduce_only,
quote_quantity,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
UUID4::new(),
UnixNanos::default(),
))
}
fn make_market(side: OrderSide, quote_quantity: bool) -> OrderAny {
OrderAny::Market(MarketOrder::new(
TraderId::from("TESTER-001"),
StrategyId::from("S-001"),
InstrumentId::from("TEST.POLYMARKET"),
ClientOrderId::from("O-001"),
side,
Quantity::from("10"),
TimeInForce::Ioc,
UUID4::new(),
UnixNanos::default(),
false,
quote_quantity,
None,
None,
None,
None,
None,
None,
None,
None,
))
}
#[rstest]
fn test_validate_limit_order_valid() {
let order = make_limit(false, false, false, TimeInForce::Gtc);
assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
}
#[rstest]
#[case("0.000", "0.001", "0.001", "0.999")]
#[case("1.000", "0.001", "0.001", "0.999")]
#[case("0.0000", "0.0001", "0.0001", "0.9999")]
#[case("1.0000", "0.0001", "0.0001", "0.9999")]
fn test_validate_limit_order_price_out_of_range_denied(
#[case] price: &str,
#[case] tick_size: &str,
#[case] min_price: &str,
#[case] max_price: &str,
) {
let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
let err = PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size))
.unwrap_err();
assert_eq!(
err,
format!(
"Limit order price {price} outside Polymarket range [{min_price}, {max_price}]"
)
);
}
#[rstest]
#[case("0.001", "0.001")]
#[case("0.999", "0.001")]
#[case("0.0001", "0.0001")]
#[case("0.9999", "0.0001")]
#[case("0.005", "0.005")]
#[case("0.995", "0.005")]
#[case("0.0025", "0.0025")]
#[case("0.9975", "0.0025")]
fn test_validate_limit_order_price_boundary_allowed(
#[case] price: &str,
#[case] tick_size: &str,
) {
let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
assert!(
PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size)).is_ok()
);
}
#[rstest]
#[case("0.505", "0.005")]
#[case("0.5025", "0.0025")]
fn test_validate_limit_order_price_aligned_allowed(
#[case] price: &str,
#[case] tick_size: &str,
) {
let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
assert!(
PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size)).is_ok()
);
}
#[rstest]
#[case("0.501", "0.005")]
#[case("0.501", "0.0025")]
fn test_validate_limit_order_price_misaligned_denied(
#[case] price: &str,
#[case] tick_size: &str,
) {
let order = make_limit_at_price(false, false, false, TimeInForce::Gtc, Price::from(price));
let error = PolymarketOrderBuilder::validate_limit_price(&order, Price::from(tick_size))
.unwrap_err();
assert_eq!(
error,
format!(
"Limit order price {price} does not conform to Polymarket tick size {tick_size}"
)
);
}
#[rstest]
fn test_validate_limit_order_reduce_only_denied() {
let order = make_limit(true, false, false, TimeInForce::Gtc);
let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
assert!(err.contains("Reduce-only"));
}
#[rstest]
fn test_validate_limit_order_quote_quantity_denied() {
let order = make_limit(false, true, false, TimeInForce::Gtc);
let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
assert!(err.contains("Quote quantity"));
}
#[rstest]
fn test_validate_limit_order_post_only_ioc_denied() {
let order = make_limit(false, false, true, TimeInForce::Ioc);
let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
assert!(err.contains("Post-only"));
}
#[rstest]
fn test_validate_limit_order_post_only_gtc_allowed() {
let order = make_limit(false, false, true, TimeInForce::Gtc);
assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
}
#[rstest]
fn test_validate_limit_order_no_order_side_denied() {
let order = OrderAny::Limit(LimitOrder::new(
TraderId::from("TESTER-001"),
StrategyId::from("S-001"),
InstrumentId::from("TEST.POLYMARKET"),
ClientOrderId::from("O-NO-SIDE"),
OrderSide::NoOrderSide,
Quantity::from("10"),
Price::from("0.50"),
TimeInForce::Gtc,
None,
false,
false,
false,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
UUID4::new(),
UnixNanos::default(),
));
let err = PolymarketOrderBuilder::validate_limit_order(&order).unwrap_err();
assert!(err.contains("Invalid order side"));
}
#[rstest]
fn test_validate_market_order_buy_with_quote_qty() {
let order = make_market(OrderSide::Buy, true);
assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
}
#[rstest]
fn test_validate_market_order_buy_without_quote_qty_denied() {
let order = make_market(OrderSide::Buy, false);
let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
assert!(err.contains("quote_quantity=true"));
}
#[rstest]
fn test_validate_market_order_sell_without_quote_qty() {
let order = make_market(OrderSide::Sell, false);
assert!(PolymarketOrderBuilder::validate_market_order(&order).is_ok());
}
#[rstest]
fn test_validate_market_order_sell_with_quote_qty_denied() {
let order = make_market(OrderSide::Sell, true);
let err = PolymarketOrderBuilder::validate_market_order(&order).unwrap_err();
assert!(err.contains("quote_quantity=false"));
}
#[rstest]
fn test_validate_market_order_wrong_type_denied() {
let limit = make_limit(false, false, false, TimeInForce::Gtc);
let err = PolymarketOrderBuilder::validate_market_order(&limit).unwrap_err();
assert!(err.contains("Expected Market order"));
}
fn make_test_builder() -> PolymarketOrderBuilder {
let pk = EvmPrivateKey::new(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
)
.unwrap();
let signer = OrderSigner::new(&pk).unwrap();
let addr = format!("{:#x}", signer.address());
PolymarketOrderBuilder::new(signer, addr.clone(), addr, SignatureType::Eoa)
}
#[rstest]
fn test_next_timestamp_ms_is_strictly_monotonic() {
let builder = make_test_builder();
let mut prev = builder.next_timestamp_ms();
for _ in 0..1_000 {
let next = builder.next_timestamp_ms();
assert!(
next > prev,
"timestamp not strictly monotonic: {prev} >= {next}"
);
prev = next;
}
}
#[rstest]
fn test_build_orders_produce_unique_timestamps() {
let builder = make_test_builder();
let mut timestamps = ahash::AHashSet::new();
for _ in 0..50 {
let order = builder
.build_limit_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
PolymarketOrderSide::Buy,
dec!(0.50),
dec!(10),
PolymarketOrderType::GTC,
"0",
false,
2,
)
.unwrap();
assert!(
timestamps.insert(order.timestamp.clone()),
"duplicate timestamp {} in burst",
order.timestamp,
);
}
}
#[rstest]
fn test_built_order_carries_nautilus_builder_code() {
let builder = make_test_builder();
let order = builder
.build_limit_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
PolymarketOrderSide::Buy,
dec!(0.50),
dec!(10),
PolymarketOrderType::GTC,
"0",
false,
2,
)
.unwrap();
assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
}
#[rstest]
fn test_poly_1271_order_uses_deposit_wallet_as_signer() {
let pk = EvmPrivateKey::new(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
)
.unwrap();
let signer = OrderSigner::new(&pk).unwrap();
let signer_address = format!("{:#x}", signer.address());
let deposit_wallet = "0x1111111111111111111111111111111111111111".to_string();
let builder = PolymarketOrderBuilder::new(
signer,
signer_address,
deposit_wallet.clone(),
SignatureType::Poly1271,
);
let order = builder
.build_limit_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
PolymarketOrderSide::Buy,
dec!(0.50),
dec!(10),
PolymarketOrderType::GTC,
"0",
false,
2,
)
.unwrap();
assert_eq!(order.maker, deposit_wallet);
assert_eq!(order.signer, deposit_wallet);
assert_eq!(order.signature_type, SignatureType::Poly1271);
assert_eq!(order.signature.len(), 636);
}
#[rstest]
fn test_build_limit_order_expiration_passthrough() {
let builder = make_test_builder();
let order = builder
.build_limit_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
PolymarketOrderSide::Buy,
dec!(0.50),
dec!(10),
PolymarketOrderType::GTD,
"1735689600",
false,
2,
)
.unwrap();
assert_eq!(order.expiration, "1735689600");
}
#[rstest]
fn test_build_limit_order_amount_matrix(
#[values(1, 2, 3, 4)] tick_decimals: u32,
#[values(PolymarketOrderSide::Buy, PolymarketOrderSide::Sell)] side: PolymarketOrderSide,
#[values(
PolymarketOrderType::GTC,
PolymarketOrderType::GTD,
PolymarketOrderType::FAK,
PolymarketOrderType::FOK
)]
order_type: PolymarketOrderType,
) {
let (price, quantity, notional) = match tick_decimals {
1 => (dec!(0.5), dec!(10), dec!(5_000_000)),
2 => (dec!(0.56), dec!(10), dec!(5_600_000)),
3 => (dec!(0.961), dec!(10), dec!(9_610_000)),
4 => (dec!(0.9612), dec!(25), dec!(24_030_000)),
_ => unreachable!(),
};
let quantity_amount = to_fixed_decimal(quantity);
let expiration = if order_type == PolymarketOrderType::GTD {
"1735689600"
} else {
"0"
};
let builder = make_test_builder();
let order = builder
.build_limit_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
side,
price,
quantity,
order_type,
expiration,
false,
tick_decimals,
)
.unwrap();
let expected = match side {
PolymarketOrderSide::Buy => (notional, quantity_amount),
PolymarketOrderSide::Sell => (quantity_amount, notional),
};
assert_eq!((order.maker_amount, order.taker_amount), expected);
}
#[rstest]
fn test_build_limit_order_denies_unrepresentable_immediate_buy(
#[values(1, 2, 3, 4)] tick_decimals: u32,
#[values(PolymarketOrderType::FAK, PolymarketOrderType::FOK)]
order_type: PolymarketOrderType,
) {
let (price, quantity, maker_amount) = match tick_decimals {
1 => (dec!(0.5), dec!(1.01), dec!(0.505)),
2 => (dec!(0.56), dec!(1.01), dec!(0.5656)),
3 => (dec!(0.961), dec!(5), dec!(4.805)),
4 => (dec!(0.9612), dec!(5), dec!(4.806)),
_ => unreachable!(),
};
let builder = make_test_builder();
let error = builder
.build_limit_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
PolymarketOrderSide::Buy,
price,
quantity,
order_type,
"0",
false,
tick_decimals,
)
.unwrap_err();
assert_eq!(
error.to_string(),
format!(
"Polymarket {order_type} BUY maker amount {maker_amount} pUSD exceeds 2 decimal places for price {price} and quantity {quantity}"
)
);
}
#[rstest]
#[case::gtc_buy(
PolymarketOrderType::GTC,
PolymarketOrderSide::Buy,
dec!(4_805_000),
dec!(5_000_000),
)]
#[case::gtd_buy(
PolymarketOrderType::GTD,
PolymarketOrderSide::Buy,
dec!(4_805_000),
dec!(5_000_000),
)]
#[case::fak_sell(
PolymarketOrderType::FAK,
PolymarketOrderSide::Sell,
dec!(5_000_000),
dec!(4_805_000),
)]
#[case::fok_sell(
PolymarketOrderType::FOK,
PolymarketOrderSide::Sell,
dec!(5_000_000),
dec!(4_805_000),
)]
fn test_build_limit_order_preserves_unaffected_fractional_cent_amounts(
#[case] order_type: PolymarketOrderType,
#[case] side: PolymarketOrderSide,
#[case] expected_maker: Decimal,
#[case] expected_taker: Decimal,
) {
let builder = make_test_builder();
let order = builder
.build_limit_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
side,
dec!(0.961),
dec!(5),
order_type,
"0",
false,
3,
)
.unwrap();
assert_eq!(order.maker_amount, expected_maker);
assert_eq!(order.taker_amount, expected_taker);
}
#[rstest]
fn test_validate_limit_order_gtd_with_expire_accepted() {
let order = make_limit(false, false, false, TimeInForce::Gtd);
assert!(PolymarketOrderBuilder::validate_limit_order(&order).is_ok());
}
#[rstest]
fn test_build_market_buy_order_wire_shape() {
let builder = make_test_builder();
let order = builder
.build_market_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
PolymarketOrderSide::Buy,
dec!(0.50),
dec!(10),
false,
2,
)
.unwrap();
assert_eq!(order.expiration, "0");
assert_eq!(order.builder, POLYMARKET_NAUTILUS_BUILDER_CODE);
assert_eq!(order.metadata, ZERO_BYTES32);
assert_eq!(order.side, PolymarketOrderSide::Buy);
assert!(!order.timestamp.is_empty());
assert!(!order.signature.is_empty());
assert_eq!(order.maker_amount, dec!(10_000_000));
assert_eq!(order.taker_amount, dec!(20_000_000));
}
#[rstest]
fn test_build_market_sell_order_wire_shape() {
let builder = make_test_builder();
let order = builder
.build_market_order(
"71321045679252212594626385532706912750332728571942532289631379312455583992563",
PolymarketOrderSide::Sell,
dec!(0.50),
dec!(20),
false,
2,
)
.unwrap();
assert_eq!(order.expiration, "0");
assert_eq!(order.side, PolymarketOrderSide::Sell);
assert_eq!(order.maker_amount, dec!(20_000_000));
assert_eq!(order.taker_amount, dec!(10_000_000));
assert!(!order.signature.is_empty());
}
#[rstest]
fn test_generate_salt_uniqueness() {
let s1 = generate_salt();
let s2 = generate_salt();
assert_ne!(s1, s2);
}
#[rstest]
fn test_generate_salt_within_53_bits() {
for _ in 0..100 {
let s = generate_salt();
assert!(s < (1u64 << 53));
}
}
#[rstest]
#[case(dec!(0.50), dec!(100), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
#[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
#[case(dec!(0.75), dec!(200), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
#[case(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3, dec!(13_296_150), dec!(23_450_000))]
#[case(dec!(0.55), dec!(10), PolymarketOrderSide::Buy, 1, dec!(5_500_000), dec!(10_000_000))]
fn test_compute_maker_taker_amounts(
#[case] price: Decimal,
#[case] quantity: Decimal,
#[case] side: PolymarketOrderSide,
#[case] tick_decimals: u32,
#[case] expected_maker: Decimal,
#[case] expected_taker: Decimal,
) {
let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
assert_eq!(maker, expected_maker);
assert_eq!(taker, expected_taker);
}
#[rstest]
#[case::buy_tick_tenth(
dec!(0.5), dec!(21.04), PolymarketOrderSide::Buy, 1,
dec!(10_520_000), dec!(21_040_000),
)]
#[case::sell_tick_tenth(
dec!(0.5), dec!(21.04), PolymarketOrderSide::Sell, 1,
dec!(21_040_000), dec!(10_520_000),
)]
#[case::buy_tick_hundredth(
dec!(0.56), dec!(21.04), PolymarketOrderSide::Buy, 2,
dec!(11_782_400), dec!(21_040_000),
)]
#[case::sell_tick_hundredth(
dec!(0.56), dec!(21.04), PolymarketOrderSide::Sell, 2,
dec!(21_040_000), dec!(11_782_400),
)]
#[case::buy_decimal_accuracy_24(
dec!(0.24), dec!(15), PolymarketOrderSide::Buy, 2,
dec!(3_600_000), dec!(15_000_000),
)]
#[case::buy_decimal_accuracy_82(
dec!(0.82), dec!(101), PolymarketOrderSide::Buy, 2,
dec!(82_820_000), dec!(101_000_000),
)]
#[case::buy_decimal_accuracy_18233(
dec!(0.58), dec!(18233.33), PolymarketOrderSide::Buy, 2,
dec!(10_575_331_400), dec!(18_233_330_000),
)]
#[case::buy_tick_thousandth(
dec!(0.056), dec!(21.04), PolymarketOrderSide::Buy, 3,
dec!(1_178_240), dec!(21_040_000),
)]
#[case::sell_tick_thousandth(
dec!(0.056), dec!(21.04), PolymarketOrderSide::Sell, 3,
dec!(21_040_000), dec!(1_178_240),
)]
#[case::buy_tick_ten_thousandth(
dec!(0.0056), dec!(21.04), PolymarketOrderSide::Buy, 4,
dec!(117_824), dec!(21_040_000),
)]
#[case::sell_tick_ten_thousandth(
dec!(0.0056), dec!(21.04), PolymarketOrderSide::Sell, 4,
dec!(21_040_000), dec!(117_824),
)]
fn test_compute_maker_taker_amounts_sdk_parity(
#[case] price: Decimal,
#[case] quantity: Decimal,
#[case] side: PolymarketOrderSide,
#[case] tick_decimals: u32,
#[case] expected_maker: Decimal,
#[case] expected_taker: Decimal,
) {
let (maker, taker) = compute_maker_taker_amounts(price, quantity, side, tick_decimals);
assert_eq!(maker, expected_maker);
assert_eq!(taker, expected_taker);
}
#[rstest]
#[case(dec!(0.50), dec!(50), PolymarketOrderSide::Buy, 2, dec!(50_000_000), dec!(100_000_000))]
#[case(dec!(0.50), dec!(100), PolymarketOrderSide::Sell, 2, dec!(100_000_000), dec!(50_000_000))]
#[case(dec!(0.75), dec!(150), PolymarketOrderSide::Buy, 2, dec!(150_000_000), dec!(200_000_000))]
#[case(dec!(0.96), dec!(5.208), PolymarketOrderSide::Sell, 2, dec!(5_200_000), dec!(4_992_000))]
#[case(dec!(0.211), dec!(23.696681), PolymarketOrderSide::Sell, 3, dec!(23_690_000), dec!(4_998_590))]
fn test_compute_market_maker_taker_amounts(
#[case] price: Decimal,
#[case] amount: Decimal,
#[case] side: PolymarketOrderSide,
#[case] tick_decimals: u32,
#[case] expected_maker: Decimal,
#[case] expected_taker: Decimal,
) {
let (maker, taker) = compute_market_maker_taker_amounts(price, amount, side, tick_decimals);
assert_eq!(maker, expected_maker);
assert_eq!(taker, expected_taker);
}
#[rstest]
fn test_compute_maker_taker_qty_truncated_to_lot_size() {
let (maker, taker) =
compute_maker_taker_amounts(dec!(0.567), dec!(23.456), PolymarketOrderSide::Buy, 3);
assert_eq!(maker, dec!(13_296_150));
assert_eq!(taker, dec!(23_450_000));
}
#[rstest]
fn test_compute_maker_taker_zero_amounts() {
let (maker, taker) =
compute_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
assert_eq!(maker, dec!(0));
assert_eq!(taker, dec!(0));
}
#[rstest]
fn test_compute_maker_taker_tick_decimals_1() {
let (maker, taker) =
compute_maker_taker_amounts(dec!(0.3), dec!(50), PolymarketOrderSide::Buy, 1);
assert_eq!(maker, dec!(15_000_000));
assert_eq!(taker, dec!(50_000_000));
}
#[rstest]
fn test_compute_maker_taker_tick_decimals_3_sell() {
let (maker, taker) =
compute_maker_taker_amounts(dec!(0.789), dec!(15.123), PolymarketOrderSide::Sell, 3);
assert_eq!(maker, dec!(15_120_000));
assert_eq!(taker, dec!(11_929_680));
}
#[rstest]
fn test_compute_market_maker_taker_zero_amount() {
let (maker, taker) =
compute_market_maker_taker_amounts(dec!(0.50), dec!(0), PolymarketOrderSide::Buy, 2);
assert_eq!(maker, dec!(0));
assert_eq!(taker, dec!(0));
}
#[rstest]
fn test_compute_market_sell_position_close() {
let (maker, taker) = compute_market_maker_taker_amounts(
dec!(0.211),
dec!(23.696681),
PolymarketOrderSide::Sell,
3,
);
assert_eq!(maker, dec!(23_690_000));
assert_eq!(taker, dec!(4_998_590));
assert_eq!(maker % dec!(10_000), dec!(0));
assert_eq!(taker % dec!(10), dec!(0));
}
#[rstest]
fn test_to_fixed_decimal_basic() {
assert_eq!(to_fixed_decimal(dec!(13.29955)), dec!(13_299_550));
assert_eq!(to_fixed_decimal(dec!(100)), dec!(100_000_000));
assert_eq!(to_fixed_decimal(dec!(0)), dec!(0));
}
}