1use std::{collections::HashMap, result::Result as StdResult, str::from_utf8};
19
20use nautilus_core::{
21 consts::NAUTILUS_USER_AGENT,
22 time::{AtomicTime, get_atomic_clock_realtime},
23};
24use nautilus_model::{
25 data::BookOrder,
26 enums::{BookType, OrderSide},
27 identifiers::InstrumentId,
28 orderbook::OrderBook,
29};
30use nautilus_network::http::{HttpClient, HttpClientError, Method, USER_AGENT};
31use serde::{Serialize, de::DeserializeOwned};
32
33use crate::{
34 common::{credential::Credential, enums::PolymarketOrderType, urls::clob_http_url},
35 http::{
36 error::{Error, Result},
37 models::{
38 ClobBookResponse, ClobMarketResponse, FeeRateResponse, PolymarketOpenOrder,
39 PolymarketOrder, PolymarketTradeReport, TickSizeResponse,
40 },
41 query::{
42 BalanceAllowance, BatchCancelResponse, CancelMarketOrdersParams, CancelResponse,
43 GetBalanceAllowanceParams, GetOrdersParams, GetTradesParams, OrderResponse,
44 PaginatedResponse,
45 },
46 rate_limits::POLYMARKET_CLOB_REST_QUOTA,
47 },
48 websocket::parse::{parse_price, parse_quantity},
49};
50
51const CURSOR_START: &str = "MA==";
52const CURSOR_END: &str = "LTE=";
53
54const PATH_ORDERS: &str = "/data/orders";
55const PATH_TRADES: &str = "/data/trades";
56const PATH_BALANCE_ALLOWANCE: &str = "/balance-allowance";
57const PATH_POST_ORDER: &str = "/order";
58const PATH_POST_ORDERS: &str = "/orders";
59const PATH_CANCEL_ALL: &str = "/cancel-all";
60const PATH_CANCEL_MARKET_ORDERS: &str = "/cancel-market-orders";
61
62#[derive(Serialize)]
63#[serde(rename_all = "camelCase")]
64struct PostOrderBody<'a> {
65 order: &'a PolymarketOrder,
66 owner: &'a str,
67 order_type: PolymarketOrderType,
68 #[serde(skip_serializing_if = "std::ops::Not::not")]
69 post_only: bool,
70}
71
72#[derive(Serialize)]
73struct CancelOrderBody<'a> {
74 #[serde(rename = "orderID")]
75 order_id: &'a str,
76}
77
78#[derive(Debug, Clone)]
84pub struct PolymarketClobHttpClient {
85 client: HttpClient,
86 base_url: String,
87 credential: Credential,
88 address: String,
89 clock: &'static AtomicTime,
90}
91
92impl PolymarketClobHttpClient {
93 pub fn new(
99 credential: Credential,
100 address: String,
101 base_url: Option<String>,
102 timeout_secs: u64,
103 ) -> StdResult<Self, HttpClientError> {
104 Ok(Self {
105 client: HttpClient::new(
106 Self::default_headers(),
107 vec![],
108 vec![],
109 Some(*POLYMARKET_CLOB_REST_QUOTA),
110 Some(timeout_secs),
111 None,
112 )?,
113 base_url: base_url
114 .unwrap_or_else(|| clob_http_url().to_string())
115 .trim_end_matches('/')
116 .to_string(),
117 credential,
118 address,
119 clock: get_atomic_clock_realtime(),
120 })
121 }
122
123 fn default_headers() -> HashMap<String, String> {
124 HashMap::from([
125 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
126 ("Content-Type".to_string(), "application/json".to_string()),
127 ])
128 }
129
130 fn url(&self, path: &str) -> String {
131 format!("{}{path}", self.base_url)
132 }
133
134 fn timestamp(&self) -> String {
135 (self.clock.get_time_ns().as_u64() / 1_000_000_000).to_string()
136 }
137
138 fn auth_headers(&self, method: &str, path: &str, body: &str) -> HashMap<String, String> {
139 let timestamp = self.timestamp();
140 let signature = self.credential.sign(×tamp, method, path, body);
141
142 HashMap::from([
143 ("POLY_ADDRESS".to_string(), self.address.clone()),
144 ("POLY_SIGNATURE".to_string(), signature),
145 ("POLY_TIMESTAMP".to_string(), timestamp),
146 (
147 "POLY_API_KEY".to_string(),
148 self.credential.api_key().to_string(),
149 ),
150 (
151 "POLY_PASSPHRASE".to_string(),
152 self.credential.passphrase().to_string(),
153 ),
154 ])
155 }
156
157 async fn send_get<P: Serialize, T: DeserializeOwned>(
158 &self,
159 path: &str,
160 params: Option<&P>,
161 auth: bool,
162 ) -> Result<T> {
163 let headers = if auth {
164 Some(self.auth_headers("GET", path, ""))
165 } else {
166 None
167 };
168 let url = self.url(path);
169 let response = self
170 .client
171 .request_with_params(Method::GET, url, params, headers, None, None, None)
172 .await
173 .map_err(Error::from_http_client)?;
174
175 if response.status.is_success() {
176 serde_json::from_slice(&response.body).map_err(Error::Serde)
177 } else {
178 Err(Error::from_status_code(
179 response.status.as_u16(),
180 &response.body,
181 ))
182 }
183 }
184
185 async fn send_get_optional<P: Serialize, T: DeserializeOwned>(
188 &self,
189 path: &str,
190 params: Option<&P>,
191 auth: bool,
192 ) -> Result<Option<T>> {
193 let headers = if auth {
194 Some(self.auth_headers("GET", path, ""))
195 } else {
196 None
197 };
198 let url = self.url(path);
199 let response = self
200 .client
201 .request_with_params(Method::GET, url, params, headers, None, None, None)
202 .await
203 .map_err(Error::from_http_client)?;
204
205 if response.status.is_success() {
206 if response.body.is_empty() || response.body.as_ref() == b"null" {
207 Ok(None)
208 } else {
209 serde_json::from_slice(&response.body)
210 .map(Some)
211 .map_err(Error::Serde)
212 }
213 } else {
214 Err(Error::from_status_code(
215 response.status.as_u16(),
216 &response.body,
217 ))
218 }
219 }
220
221 async fn send_post<T: DeserializeOwned>(&self, path: &str, body_bytes: Vec<u8>) -> Result<T> {
222 let body_str =
223 from_utf8(&body_bytes).map_err(|e| Error::decode(format!("UTF-8 error: {e}")))?;
224 let headers = Some(self.auth_headers("POST", path, body_str));
225 let url = self.url(path);
226 let response = self
227 .client
228 .request(
229 Method::POST,
230 url,
231 None,
232 headers,
233 Some(body_bytes),
234 None,
235 None,
236 )
237 .await
238 .map_err(Error::from_http_client)?;
239
240 if response.status.is_success() {
241 serde_json::from_slice(&response.body).map_err(Error::Serde)
242 } else {
243 Err(Error::from_status_code(
244 response.status.as_u16(),
245 &response.body,
246 ))
247 }
248 }
249
250 async fn send_delete<T: DeserializeOwned>(
251 &self,
252 path: &str,
253 body_bytes: Option<Vec<u8>>,
254 ) -> Result<T> {
255 let body_str = body_bytes
256 .as_deref()
257 .map(|b| from_utf8(b).map_err(|e| Error::decode(format!("UTF-8 error: {e}"))))
258 .transpose()?
259 .unwrap_or("");
260 let headers = Some(self.auth_headers("DELETE", path, body_str));
261 let url = self.url(path);
262 let response = self
263 .client
264 .request(Method::DELETE, url, None, headers, body_bytes, None, None)
265 .await
266 .map_err(Error::from_http_client)?;
267
268 if response.status.is_success() {
269 serde_json::from_slice(&response.body).map_err(Error::Serde)
270 } else {
271 Err(Error::from_status_code(
272 response.status.as_u16(),
273 &response.body,
274 ))
275 }
276 }
277
278 pub async fn get_orders(
280 &self,
281 mut params: GetOrdersParams,
282 ) -> Result<Vec<PolymarketOpenOrder>> {
283 if params.next_cursor.is_none() {
284 params.next_cursor = Some(CURSOR_START.to_string());
285 }
286 let mut all = Vec::new();
287
288 loop {
289 let page: PaginatedResponse<PolymarketOpenOrder> =
290 self.send_get(PATH_ORDERS, Some(¶ms), true).await?;
291 all.extend(page.data);
292 if page.next_cursor == CURSOR_END {
293 break;
294 }
295 params.next_cursor = Some(page.next_cursor);
296 }
297 Ok(all)
298 }
299
300 pub async fn get_order_optional(&self, order_id: &str) -> Result<Option<PolymarketOpenOrder>> {
302 let path = format!("/data/order/{order_id}");
303 self.send_get_optional::<(), _>(&path, None::<&()>, true)
304 .await
305 }
306
307 pub async fn get_order(&self, order_id: &str) -> Result<PolymarketOpenOrder> {
311 self.get_order_optional(order_id)
312 .await?
313 .ok_or_else(|| Error::decode(format!("Order {order_id} not found (empty response)")))
314 }
315
316 pub async fn get_trades(
318 &self,
319 mut params: GetTradesParams,
320 ) -> Result<Vec<PolymarketTradeReport>> {
321 if params.next_cursor.is_none() {
322 params.next_cursor = Some(CURSOR_START.to_string());
323 }
324 let mut all = Vec::new();
325
326 loop {
327 let page: PaginatedResponse<PolymarketTradeReport> =
328 self.send_get(PATH_TRADES, Some(¶ms), true).await?;
329 all.extend(page.data);
330 if page.next_cursor == CURSOR_END {
331 break;
332 }
333 params.next_cursor = Some(page.next_cursor);
334 }
335 Ok(all)
336 }
337
338 pub async fn get_balance_allowance(
340 &self,
341 params: GetBalanceAllowanceParams,
342 ) -> Result<BalanceAllowance> {
343 let headers = Some(self.auth_headers("GET", PATH_BALANCE_ALLOWANCE, ""));
344 let url = self.url(PATH_BALANCE_ALLOWANCE);
345 let response = self
346 .client
347 .request_with_params(Method::GET, url, Some(¶ms), headers, None, None, None)
348 .await
349 .map_err(Error::from_http_client)?;
350
351 if response.status.is_success() {
352 serde_json::from_slice(&response.body).map_err(Error::Serde)
353 } else {
354 Err(Error::from_status_code(
355 response.status.as_u16(),
356 &response.body,
357 ))
358 }
359 }
360
361 pub async fn post_order(
363 &self,
364 order: &PolymarketOrder,
365 order_type: PolymarketOrderType,
366 post_only: bool,
367 ) -> Result<OrderResponse> {
368 let owner = self.credential.api_key().to_string();
369 let body = PostOrderBody {
370 order,
371 owner: &owner,
372 order_type,
373 post_only,
374 };
375 let body_bytes = serde_json::to_vec(&body).map_err(Error::Serde)?;
376 self.send_post(PATH_POST_ORDER, body_bytes).await
377 }
378
379 pub async fn post_orders(
383 &self,
384 orders: &[(&PolymarketOrder, PolymarketOrderType, bool)],
385 ) -> Result<Vec<OrderResponse>> {
386 let owner = self.credential.api_key().to_string();
387 let entries: Vec<PostOrderBody<'_>> = orders
388 .iter()
389 .map(|(order, order_type, post_only)| PostOrderBody {
390 order,
391 owner: &owner,
392 order_type: *order_type,
393 post_only: *post_only,
394 })
395 .collect();
396 let body_bytes = serde_json::to_vec(&entries).map_err(Error::Serde)?;
397 self.send_post(PATH_POST_ORDERS, body_bytes).await
398 }
399
400 pub async fn cancel_order(&self, order_id: &str) -> Result<CancelResponse> {
402 let body = CancelOrderBody { order_id };
403 let body_bytes = serde_json::to_vec(&body).map_err(Error::Serde)?;
404 self.send_delete("/order", Some(body_bytes)).await
405 }
406
407 pub async fn cancel_orders(&self, order_ids: &[&str]) -> Result<BatchCancelResponse> {
409 let body_bytes = serde_json::to_vec(order_ids).map_err(Error::Serde)?;
410 self.send_delete("/orders", Some(body_bytes)).await
411 }
412
413 pub async fn cancel_all(&self) -> Result<BatchCancelResponse> {
415 self.send_delete(PATH_CANCEL_ALL, None).await
416 }
417
418 pub async fn cancel_market_orders(
420 &self,
421 params: CancelMarketOrdersParams,
422 ) -> Result<BatchCancelResponse> {
423 let body_bytes = serde_json::to_vec(¶ms).map_err(Error::Serde)?;
424 self.send_delete(PATH_CANCEL_MARKET_ORDERS, Some(body_bytes))
425 .await
426 }
427
428 pub async fn get_tick_size(&self, token_id: &str) -> Result<TickSizeResponse> {
430 let params = [("token_id", token_id)];
431 self.send_get("/tick-size", Some(¶ms), false).await
432 }
433
434 pub async fn get_fee_rate(&self, token_id: &str) -> Result<FeeRateResponse> {
436 let params = [("token_id", token_id)];
437 self.send_get("/fee-rate", Some(¶ms), false).await
438 }
439
440 pub async fn get_book(&self, token_id: &str) -> Result<ClobBookResponse> {
442 let params = [("token_id", token_id)];
443 self.send_get("/book", Some(¶ms), false).await
444 }
445}
446
447#[derive(Debug, Clone)]
452pub struct PolymarketClobPublicClient {
453 client: HttpClient,
454 base_url: String,
455}
456
457impl PolymarketClobPublicClient {
458 pub fn new(base_url: Option<String>, timeout_secs: u64) -> StdResult<Self, HttpClientError> {
464 Ok(Self {
465 client: HttpClient::new(
466 HashMap::from([
467 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
468 ("Content-Type".to_string(), "application/json".to_string()),
469 ]),
470 vec![],
471 vec![],
472 Some(*POLYMARKET_CLOB_REST_QUOTA),
473 Some(timeout_secs),
474 None,
475 )?,
476 base_url: base_url
477 .unwrap_or_else(|| clob_http_url().to_string())
478 .trim_end_matches('/')
479 .to_string(),
480 })
481 }
482
483 pub async fn get_book(&self, token_id: &str) -> Result<ClobBookResponse> {
485 let params = [("token_id", token_id)];
486 let url = format!("{}/book", self.base_url);
487 let response = self
488 .client
489 .request_with_params(Method::GET, url, Some(¶ms), None, None, None, None)
490 .await
491 .map_err(Error::from_http_client)?;
492
493 if response.status.is_success() {
494 serde_json::from_slice(&response.body).map_err(Error::Serde)
495 } else {
496 Err(Error::from_status_code(
497 response.status.as_u16(),
498 &response.body,
499 ))
500 }
501 }
502
503 pub async fn get_market(&self, condition_id: &str) -> Result<ClobMarketResponse> {
505 let url = format!("{}/markets/{condition_id}", self.base_url);
506 let response = self
507 .client
508 .request_with_params(Method::GET, url, None::<&()>, None, None, None, None)
509 .await
510 .map_err(Error::from_http_client)?;
511
512 if response.status.is_success() {
513 serde_json::from_slice(&response.body).map_err(Error::Serde)
514 } else {
515 Err(Error::from_status_code(
516 response.status.as_u16(),
517 &response.body,
518 ))
519 }
520 }
521
522 pub async fn request_book_snapshot(
524 &self,
525 instrument_id: InstrumentId,
526 token_id: &str,
527 price_precision: u8,
528 size_precision: u8,
529 ) -> anyhow::Result<OrderBook> {
530 let resp = self
531 .get_book(token_id)
532 .await
533 .map_err(|e| anyhow::anyhow!(e))?;
534
535 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
536
537 for (i, level) in resp.bids.iter().enumerate() {
538 let price = parse_price(&level.price, price_precision)?;
539 let size = parse_quantity(&level.size, size_precision)?;
540 let order = BookOrder::new(OrderSide::Buy, price, size, i as u64);
541 book.add(order, 0, i as u64, Default::default());
542 }
543
544 let bids_len = resp.bids.len();
545 for (i, level) in resp.asks.iter().enumerate() {
546 let price = parse_price(&level.price, price_precision)?;
547 let size = parse_quantity(&level.size, size_precision)?;
548 let order = BookOrder::new(OrderSide::Sell, price, size, (bids_len + i) as u64);
549 book.add(order, 0, (bids_len + i) as u64, Default::default());
550 }
551
552 log::debug!(
553 "Fetched order book for {} with {} bids and {} asks",
554 instrument_id,
555 resp.bids.len(),
556 resp.asks.len(),
557 );
558
559 Ok(book)
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use nautilus_model::{
566 enums::{BookType, OrderSide},
567 identifiers::InstrumentId,
568 types::{Price, Quantity},
569 };
570 use rstest::rstest;
571
572 use super::*;
573 use crate::http::models::{ClobBookLevel, ClobBookResponse};
574
575 fn build_book_from_response(resp: &ClobBookResponse) -> OrderBook {
576 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
577 let price_precision = 2u8;
578 let size_precision = 2u8;
579 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
580
581 for (i, level) in resp.bids.iter().enumerate() {
582 let price = parse_price(&level.price, price_precision).unwrap();
583 let size = parse_quantity(&level.size, size_precision).unwrap();
584 let order = BookOrder::new(OrderSide::Buy, price, size, i as u64);
585 book.add(order, 0, i as u64, Default::default());
586 }
587
588 let bids_len = resp.bids.len();
589 for (i, level) in resp.asks.iter().enumerate() {
590 let price = parse_price(&level.price, price_precision).unwrap();
591 let size = parse_quantity(&level.size, size_precision).unwrap();
592 let order = BookOrder::new(OrderSide::Sell, price, size, (bids_len + i) as u64);
593 book.add(order, 0, (bids_len + i) as u64, Default::default());
594 }
595
596 book
597 }
598
599 #[rstest]
600 fn test_build_order_book_from_clob_response() {
601 let resp = ClobBookResponse {
602 bids: vec![
603 ClobBookLevel {
604 price: "0.48".to_string(),
605 size: "100.00".to_string(),
606 },
607 ClobBookLevel {
608 price: "0.49".to_string(),
609 size: "200.00".to_string(),
610 },
611 ClobBookLevel {
612 price: "0.50".to_string(),
613 size: "150.00".to_string(),
614 },
615 ],
616 asks: vec![
617 ClobBookLevel {
618 price: "0.51".to_string(),
619 size: "120.00".to_string(),
620 },
621 ClobBookLevel {
622 price: "0.52".to_string(),
623 size: "180.00".to_string(),
624 },
625 ],
626 };
627
628 let book = build_book_from_response(&resp);
629
630 assert_eq!(book.instrument_id, InstrumentId::from("TEST.POLYMARKET"));
631 assert_eq!(book.book_type, BookType::L2_MBP);
632 assert_eq!(book.best_bid_price(), Some(Price::from("0.50")));
633 assert_eq!(book.best_ask_price(), Some(Price::from("0.51")));
634 assert_eq!(book.best_bid_size(), Some(Quantity::from("150.00")));
635 assert_eq!(book.best_ask_size(), Some(Quantity::from("120.00")));
636 assert_eq!(book.bids(None).count(), 3);
637 assert_eq!(book.asks(None).count(), 2);
638 }
639
640 #[rstest]
641 fn test_build_order_book_empty_response() {
642 let resp = ClobBookResponse {
643 bids: vec![],
644 asks: vec![],
645 };
646
647 let book = build_book_from_response(&resp);
648
649 assert!(book.best_bid_price().is_none());
650 assert!(book.best_ask_price().is_none());
651 }
652}