use std::sync::Arc;
use nautilus_core::UnixNanos;
use nautilus_model::{
enums::{OrderSide, TimeInForce},
types::{Price, Quantity},
};
use nautilus_network::retry::{RetryConfig, RetryManager};
use rust_decimal::Decimal;
use super::{
order_builder::PolymarketOrderBuilder,
parse::{adjust_market_buy_amount, calculate_market_price},
types::{LimitOrderSubmitRequest, SignedLimitOrderSubmission},
};
use crate::{
common::enums::{PolymarketOrderSide, PolymarketOrderType},
http::{
clob::PolymarketClobHttpClient,
error::Error,
models::{PolymarketOpenOrder, PolymarketOrder},
query::{CancelResponse, OrderResponse},
},
};
#[derive(Debug, Clone)]
pub(crate) struct MarketBuyFeeContext {
pub user_pusd_balance: Decimal,
pub fee_rate: Decimal,
pub fee_exponent: f64,
pub builder_taker_fee_rate: Decimal,
}
#[derive(Debug, Clone)]
pub(crate) struct OrderSubmitter {
http_client: PolymarketClobHttpClient,
order_builder: Arc<PolymarketOrderBuilder>,
retry_manager: Arc<RetryManager<Error>>,
}
impl OrderSubmitter {
pub fn new(
http_client: PolymarketClobHttpClient,
order_builder: Arc<PolymarketOrderBuilder>,
retry_config: RetryConfig,
) -> Self {
Self {
http_client,
order_builder,
retry_manager: Arc::new(RetryManager::new(retry_config)),
}
}
#[expect(clippy::too_many_arguments)]
pub async fn submit_limit_order(
&self,
token_id: &str,
side: OrderSide,
price: Price,
quantity: Quantity,
time_in_force: TimeInForce,
post_only: bool,
neg_risk: bool,
expire_time: Option<UnixNanos>,
tick_decimals: u32,
) -> anyhow::Result<OrderResponse> {
let request = LimitOrderSubmitRequest {
token_id: token_id.to_string(),
side,
price,
quantity,
time_in_force,
post_only,
neg_risk,
expire_time,
tick_decimals,
};
let submission = self.prepare_limit_order_submission(&request).await?;
self.post_limit_order_submission(submission).await
}
pub async fn submit_market_order(
&self,
token_id: &str,
side: OrderSide,
amount: Quantity,
neg_risk: bool,
tick_decimals: u32,
fee_context: Option<MarketBuyFeeContext>,
) -> anyhow::Result<(OrderResponse, Decimal)> {
let poly_side = PolymarketOrderSide::try_from(side)
.map_err(|e| anyhow::anyhow!("Invalid order side: {e}"))?;
let amount_dec = amount.as_decimal();
let book = self
.http_client
.get_book(token_id)
.await
.map_err(|e| anyhow::anyhow!("Failed to fetch order book: {e}"))?;
let levels = match poly_side {
PolymarketOrderSide::Buy => &book.asks,
PolymarketOrderSide::Sell => &book.bids,
};
let result = calculate_market_price(levels, amount_dec, poly_side)
.map_err(|e| anyhow::anyhow!("Market price calculation failed: {e}"))?;
let signed_amount = match (poly_side, fee_context) {
(PolymarketOrderSide::Buy, Some(ctx)) => adjust_market_buy_amount(
amount_dec,
ctx.user_pusd_balance,
result.crossing_price,
ctx.fee_rate,
ctx.fee_exponent,
ctx.builder_taker_fee_rate,
)?,
_ => amount_dec,
};
let poly_order = self
.order_builder
.build_market_order(
token_id,
poly_side,
result.crossing_price,
signed_amount,
neg_risk,
tick_decimals,
)
.map_err(|e| anyhow::anyhow!("Failed to build market order: {e}"))?;
let usdc_scale = Decimal::from(1_000_000u32);
let signed_base_qty = match poly_side {
PolymarketOrderSide::Buy => poly_order.taker_amount / usdc_scale,
PolymarketOrderSide::Sell => amount_dec,
};
let http_client = self.http_client.clone();
let response = self
.retry_manager
.execute_with_retry(
"submit_market_order",
|| {
let http_client = http_client.clone();
let poly_order = poly_order.clone();
async move {
http_client
.post_order(&poly_order, PolymarketOrderType::FOK, false)
.await
}
},
|e| e.is_retryable(),
Error::transport,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok((response, signed_base_qty))
}
pub async fn cancel_order(&self, venue_order_id: &str) -> anyhow::Result<CancelResponse> {
let http_client = self.http_client.clone();
let order_id = venue_order_id.to_string();
self.retry_manager
.execute_with_retry(
"cancel_order",
|| {
let http_client = http_client.clone();
let order_id = order_id.clone();
async move { http_client.cancel_order(&order_id).await }
},
|e| e.is_retryable(),
Error::transport,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
}
pub async fn cancel_orders(&self, venue_order_ids: &[&str]) -> anyhow::Result<CancelResponse> {
let http_client = self.http_client.clone();
let order_ids: Vec<String> = venue_order_ids.iter().map(|s| s.to_string()).collect();
self.retry_manager
.execute_with_retry(
"cancel_orders",
|| {
let http_client = http_client.clone();
let order_ids = order_ids.clone();
async move {
let refs: Vec<&str> = order_ids.iter().map(String::as_str).collect();
http_client.cancel_orders(&refs).await
}
},
|e| e.is_retryable(),
Error::transport,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
}
pub async fn get_order(&self, order_id: &str) -> anyhow::Result<Option<PolymarketOpenOrder>> {
let http_client = self.http_client.clone();
let oid = order_id.to_string();
self.retry_manager
.execute_with_retry(
"get_order",
|| {
let http_client = http_client.clone();
let oid = oid.clone();
async move { http_client.get_order_optional(&oid).await }
},
|e| e.is_retryable(),
Error::transport,
)
.await
.map_err(|e| anyhow::anyhow!("Failed to fetch order status: {e}"))
}
pub(crate) async fn prepare_limit_order_submissions(
&self,
requests: &[LimitOrderSubmitRequest],
) -> Vec<anyhow::Result<SignedLimitOrderSubmission>> {
let futures = requests
.iter()
.map(|request| self.prepare_limit_order_submission(request));
futures_util::future::join_all(futures).await
}
pub(crate) async fn prepare_limit_order_submission(
&self,
request: &LimitOrderSubmitRequest,
) -> anyhow::Result<SignedLimitOrderSubmission> {
let order_type = PolymarketOrderType::try_from(request.time_in_force)
.map_err(|e| anyhow::anyhow!("Unsupported time in force: {e}"))?;
let side = PolymarketOrderSide::try_from(request.side)
.map_err(|e| anyhow::anyhow!("Invalid order side: {e}"))?;
let expiration = limit_order_expiration(request.expire_time);
let order = self
.order_builder
.build_limit_order(
&request.token_id,
side,
request.price.as_decimal(),
request.quantity.as_decimal(),
&expiration,
request.neg_risk,
request.tick_decimals,
)
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(SignedLimitOrderSubmission {
order,
order_type,
post_only: request.post_only,
})
}
pub(crate) async fn post_limit_order_submission(
&self,
submission: SignedLimitOrderSubmission,
) -> anyhow::Result<OrderResponse> {
let http_client = self.http_client.clone();
self.retry_manager
.execute_with_retry(
"submit_limit_order",
|| {
let http_client = http_client.clone();
let submission = submission.clone();
async move {
http_client
.post_order(
&submission.order,
submission.order_type,
submission.post_only,
)
.await
}
},
|e| e.is_retryable(),
Error::transport,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
}
pub(crate) async fn post_limit_order_submissions(
&self,
submissions: Vec<SignedLimitOrderSubmission>,
) -> anyhow::Result<Vec<OrderResponse>> {
let order_refs: Vec<(&PolymarketOrder, PolymarketOrderType, bool)> = submissions
.iter()
.map(|submission| {
(
&submission.order,
submission.order_type,
submission.post_only,
)
})
.collect();
self.http_client
.post_orders(&order_refs)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
}
}
fn limit_order_expiration(expire_time: Option<UnixNanos>) -> String {
match expire_time {
Some(ns) if ns.as_u64() > 0 => (ns.as_u64() / 1_000_000_000).to_string(),
_ => "0".to_string(),
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::none(None, "0")]
#[case::zero(Some(UnixNanos::from(0u64)), "0")]
#[case::one_second(Some(UnixNanos::from(1_000_000_000u64)), "1")]
#[case::sub_second_truncates(Some(UnixNanos::from(1_500_000_000u64)), "1")]
#[case::typical(Some(UnixNanos::from(1_735_689_600_000_000_000u64)), "1735689600")]
fn test_limit_order_expiration(#[case] expire_time: Option<UnixNanos>, #[case] expected: &str) {
assert_eq!(limit_order_expiration(expire_time), expected);
}
}