1pub mod order_builder;
19pub mod parse;
20
21pub(crate) mod context;
22pub(crate) mod order_fill_tracker;
23pub(crate) mod pending;
24pub(crate) mod reconciliation;
25pub(crate) mod submitter;
26pub(crate) mod types;
27
28mod cancellations;
29mod lifecycle;
30mod orders;
31mod reports;
32mod responses;
33
34use std::sync::{Arc, atomic::AtomicBool};
35
36use ahash::AHashMap;
37use anyhow::Context;
38use async_trait::async_trait;
39use nautilus_common::{
40 clients::ExecutionClient,
41 messages::execution::{
42 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
43 GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
44 ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
45 },
46 msgbus::TypedHandler,
47};
48use nautilus_core::{
49 Params, UnixNanos,
50 collections::AtomicMap,
51 time::{AtomicTime, get_atomic_clock_realtime},
52};
53use nautilus_live::{ExecutionClientCore, ExecutionEventEmitter, SocketControl, task::TaskGroup};
54use nautilus_model::{
55 accounts::AccountAny,
56 enums::{AccountType, LiquiditySide, OmsType},
57 events::{OrderEventAny, PositionEvent},
58 identifiers::{
59 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
60 },
61 instruments::InstrumentAny,
62 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
63 types::{AccountBalance, MarginBalance, Money, Price, Quantity},
64};
65use nautilus_network::retry::RetryConfig;
66use parking_lot::Mutex;
67pub(crate) use responses::is_post_only_crossing;
68use rust_decimal::Decimal;
69use ustr::Ustr;
70
71pub(crate) use self::reports::get_pusd_currency;
72use self::{
73 context::OrderContextRegistry,
74 order_builder::PolymarketOrderBuilder,
75 order_fill_tracker::OrderFillTrackerMap,
76 pending::{PendingCancelTracker, PendingSubmitTracker},
77 submitter::OrderSubmitter,
78};
79use crate::{
80 common::{consts::POLYMARKET_VENUE, credential::Secrets, enums::PolymarketSignatureType},
81 config::PolymarketExecutionClientConfig,
82 http::{clob::PolymarketClobHttpClient, data_api::PolymarketDataApiHttpClient},
83 signing::eip712::OrderSigner,
84 websocket::{
85 USER_STREAMS_ENDPOINT, client::PolymarketWebSocketClient, dispatch::WsDispatchState,
86 },
87};
88
89#[derive(Debug)]
91pub struct PolymarketExecutionClient {
92 core: ExecutionClientCore,
93 clock: &'static AtomicTime,
94 config: PolymarketExecutionClientConfig,
95 emitter: ExecutionEventEmitter,
96 http_client: PolymarketClobHttpClient,
97 data_api_client: PolymarketDataApiHttpClient,
98 submitter: OrderSubmitter,
99 ws_client: PolymarketWebSocketClient,
100 secrets: Secrets,
101 session_tasks: TaskGroup,
102 pending_tasks: TaskGroup,
103 shutdown_errors: Vec<String>,
104 stopping: Arc<AtomicBool>,
105 heartbeat_healthy: Arc<AtomicBool>,
106 order_event_handler: Option<TypedHandler<OrderEventAny>>,
107 position_event_handler: Option<TypedHandler<PositionEvent>>,
108 shared_token_instruments: Arc<AtomicMap<Ustr, InstrumentAny>>,
109 neg_risk_index: Arc<AtomicMap<InstrumentId, bool>>,
110 pending_submits: PendingSubmitTracker,
111 pending_cancels: PendingCancelTracker,
112 order_contexts: Arc<OrderContextRegistry>,
113 order_reservations: Arc<Mutex<AHashMap<ClientOrderId, Money>>>,
114 fill_tracker: Arc<OrderFillTrackerMap>,
115 ws_dispatch_state: Arc<Mutex<WsDispatchState>>,
116}
117
118impl PolymarketExecutionClient {
119 pub fn new(
125 core: ExecutionClientCore,
126 config: PolymarketExecutionClientConfig,
127 ) -> anyhow::Result<Self> {
128 let proxy_url = config.validated_proxy_url()?;
129 config.validate_signer()?;
130 let secrets = Secrets::resolve(
131 config.private_key.clone(),
132 config.api_key.clone(),
133 config.api_secret.clone(),
134 config.passphrase.clone(),
135 config.funder.clone(),
136 )
137 .context("failed to resolve Polymarket credentials")?;
138
139 let signer_address = secrets.address.clone();
140 let maker_address = resolve_maker_address(
141 config.signature_type,
142 &signer_address,
143 secrets.funder.as_deref(),
144 )?;
145 let http_client = PolymarketClobHttpClient::new_with_proxy(
146 secrets.credential.clone(),
147 signer_address.clone(),
148 config.base_url_http.clone(),
149 config.http_timeout_secs,
150 proxy_url.clone(),
151 )
152 .map_err(|e| anyhow::anyhow!("{e}"))
153 .context("failed to create CLOB HTTP client")?;
154
155 let data_api_client = PolymarketDataApiHttpClient::new_with_proxy(
156 Some(config.data_api_url()),
157 config.http_timeout_secs,
158 proxy_url.clone(),
159 )
160 .map_err(|e| anyhow::anyhow!("{e}"))
161 .context("failed to create Data API HTTP client")?;
162
163 let order_signer = OrderSigner::new(&secrets.private_key)
164 .context("failed to create order signer")?
165 .with_signer_type(config.signer_type);
166
167 let order_builder = Arc::new(PolymarketOrderBuilder::new(
168 order_signer,
169 signer_address,
170 maker_address,
171 config.signature_type,
172 ));
173
174 let retry_config = RetryConfig {
175 max_retries: config.max_retries,
176 initial_delay_ms: config.retry_delay_initial_ms,
177 max_delay_ms: config.retry_delay_max_ms,
178 backoff_factor: 2.0,
179 jitter_ms: 1_000,
180 operation_timeout_ms: Some(config.http_timeout_secs * 1_000),
181 immediate_first: false,
182 max_elapsed_ms: Some(180_000),
183 };
184 let submitter = OrderSubmitter::new(http_client.clone(), order_builder, retry_config);
185
186 let ws_client = PolymarketWebSocketClient::new_user_with_proxy(
187 config.base_url_ws.clone(),
188 secrets.credential.clone(),
189 config.transport_backend,
190 proxy_url,
191 );
192
193 let ws_client = ws_client.with_socket_control(SocketControl::new(
194 core.client_id,
195 Some(*POLYMARKET_VENUE),
196 USER_STREAMS_ENDPOINT,
197 ));
198
199 let clock = get_atomic_clock_realtime();
200 let pusd = get_pusd_currency();
201 let emitter = ExecutionEventEmitter::new(
202 clock,
203 core.trader_id,
204 core.account_id,
205 AccountType::Cash,
206 Some(pusd),
207 );
208
209 let session_tasks = TaskGroup::new();
210 let pending_tasks = TaskGroup::new();
211
212 Ok(Self {
213 core,
214 clock,
215 config,
216 emitter,
217 http_client,
218 data_api_client,
219 submitter,
220 ws_client,
221 secrets,
222 session_tasks,
223 pending_tasks,
224 shutdown_errors: Vec::new(),
225 stopping: Arc::new(AtomicBool::new(false)),
226 heartbeat_healthy: Arc::new(AtomicBool::new(true)),
227 order_event_handler: None,
228 position_event_handler: None,
229 shared_token_instruments: Arc::new(AtomicMap::new()),
230 neg_risk_index: Arc::new(AtomicMap::new()),
231 pending_submits: PendingSubmitTracker::default(),
232 pending_cancels: PendingCancelTracker::default(),
233 order_contexts: Arc::new(OrderContextRegistry::default()),
234 order_reservations: Arc::new(Mutex::new(AHashMap::new())),
235 fill_tracker: Arc::new(OrderFillTrackerMap::new()),
236 ws_dispatch_state: Arc::new(Mutex::new(WsDispatchState::default())),
237 })
238 }
239}
240
241fn resolve_maker_address(
242 signature_type: PolymarketSignatureType,
243 signer_address: &str,
244 funder: Option<&str>,
245) -> anyhow::Result<String> {
246 let maker_address = match signature_type {
247 PolymarketSignatureType::Eoa => funder.unwrap_or(signer_address),
248 PolymarketSignatureType::PolyProxy
249 | PolymarketSignatureType::PolyGnosisSafe
250 | PolymarketSignatureType::Poly1271 => funder.ok_or_else(|| {
251 anyhow::anyhow!(
252 "Polymarket {signature_type:?} signature type requires a funder wallet address",
253 )
254 })?,
255 };
256
257 if signature_type != PolymarketSignatureType::Eoa
258 && maker_address.eq_ignore_ascii_case(signer_address)
259 {
260 anyhow::bail!(
261 "Polymarket {signature_type:?} signature type requires a funder distinct from the signing address",
262 );
263 }
264
265 Ok(maker_address.to_string())
266}
267
268#[async_trait(?Send)]
269impl ExecutionClient for PolymarketExecutionClient {
270 fn is_connected(&self) -> bool {
271 self.core.is_connected()
272 && (!self.config.heartbeat_enabled
273 || self
274 .heartbeat_healthy
275 .load(std::sync::atomic::Ordering::Acquire))
276 }
277
278 fn client_id(&self) -> ClientId {
279 self.core.client_id
280 }
281
282 fn account_id(&self) -> AccountId {
283 self.core.account_id
284 }
285
286 fn venue(&self) -> Venue {
287 *POLYMARKET_VENUE
288 }
289
290 fn oms_type(&self) -> OmsType {
291 OmsType::Netting
292 }
293
294 fn get_account(&self) -> Option<AccountAny> {
295 self.core.cache().account_owned(&self.core.account_id)
296 }
297
298 fn position_reconciliation_tolerance(&self) -> Decimal {
299 crate::common::consts::POSITION_RECONCILIATION_TOLERANCE
300 }
301
302 fn generate_account_state(
303 &self,
304 balances: Vec<AccountBalance>,
305 margins: Vec<MarginBalance>,
306 reported: bool,
307 ts_event: UnixNanos,
308 info: Option<Params>,
309 ) -> anyhow::Result<()> {
310 self.emitter
311 .emit_account_state(balances, margins, reported, ts_event, info);
312 Ok(())
313 }
314
315 fn start(&mut self) -> anyhow::Result<()> {
316 self.start_client();
317 Ok(())
318 }
319
320 fn stop(&mut self) -> anyhow::Result<()> {
321 self.stop_client();
322 Ok(())
323 }
324
325 fn reset(&mut self) -> anyhow::Result<()> {
326 self.reset_client();
327 Ok(())
328 }
329
330 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
331 self.submit_order_command(&cmd)
332 }
333
334 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
335 self.submit_order_list_command(&cmd);
336 Ok(())
337 }
338
339 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
340 self.modify_order_command(&cmd);
341 Ok(())
342 }
343
344 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
345 self.cancel_order_command(&cmd);
346 Ok(())
347 }
348
349 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
350 self.cancel_all_orders_command(&cmd)
351 }
352
353 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
354 self.batch_cancel_orders_command(&cmd);
355 Ok(())
356 }
357
358 fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
359 self.query_account_command(cmd);
360 Ok(())
361 }
362
363 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
364 self.query_order_command(&cmd);
365 Ok(())
366 }
367
368 fn register_external_order(
369 &self,
370 _client_order_id: ClientOrderId,
371 _venue_order_id: VenueOrderId,
372 _instrument_id: InstrumentId,
373 _strategy_id: StrategyId,
374 _ts_init: UnixNanos,
375 ) {
376 }
377
378 fn on_instrument(&mut self, instrument: InstrumentAny) {
379 self.on_instrument_update(&instrument);
380 }
381
382 fn calculate_commission(
383 &self,
384 instrument: &InstrumentAny,
385 last_qty: Quantity,
386 last_px: Price,
387 liquidity_side: LiquiditySide,
388 ) -> anyhow::Result<Option<Money>> {
389 self.calculate_commission_impl(instrument, last_qty, last_px, liquidity_side)
390 .map(Some)
391 }
392
393 async fn connect(&mut self) -> anyhow::Result<()> {
394 self.connect_client().await
395 }
396
397 async fn disconnect(&mut self) -> anyhow::Result<()> {
398 self.disconnect_client().await
399 }
400
401 async fn generate_order_status_report(
402 &self,
403 cmd: &GenerateOrderStatusReport,
404 ) -> anyhow::Result<Option<OrderStatusReport>> {
405 self.generate_order_status_report_impl(cmd).await
406 }
407
408 async fn generate_order_status_reports(
409 &self,
410 cmd: &GenerateOrderStatusReports,
411 ) -> anyhow::Result<Vec<OrderStatusReport>> {
412 self.generate_order_status_reports_impl(cmd).await
413 }
414
415 async fn generate_fill_reports(
416 &self,
417 cmd: GenerateFillReports,
418 ) -> anyhow::Result<Vec<FillReport>> {
419 self.generate_fill_reports_impl(cmd).await
420 }
421
422 async fn generate_position_status_reports(
423 &self,
424 cmd: &GeneratePositionStatusReports,
425 ) -> anyhow::Result<Vec<PositionStatusReport>> {
426 self.generate_position_status_reports_impl(cmd).await
427 }
428
429 async fn generate_mass_status(
430 &self,
431 lookback_mins: Option<u64>,
432 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
433 self.generate_mass_status_impl(lookback_mins).await
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use rstest::rstest;
440
441 use super::*;
442
443 #[rstest]
444 #[case(PolymarketSignatureType::PolyProxy)]
445 #[case(PolymarketSignatureType::PolyGnosisSafe)]
446 #[case(PolymarketSignatureType::Poly1271)]
447 fn proxy_signature_types_require_funder(#[case] signature_type: PolymarketSignatureType) {
448 let error = resolve_maker_address(signature_type, "0xsigner", None).unwrap_err();
449
450 assert!(
451 error
452 .to_string()
453 .contains("requires a funder wallet address")
454 );
455 }
456
457 #[rstest]
458 #[case(PolymarketSignatureType::PolyProxy)]
459 #[case(PolymarketSignatureType::PolyGnosisSafe)]
460 #[case(PolymarketSignatureType::Poly1271)]
461 fn proxy_signature_types_require_distinct_funder(
462 #[case] signature_type: PolymarketSignatureType,
463 ) {
464 let error =
465 resolve_maker_address(signature_type, "0xsigner", Some("0xSIGNER")).unwrap_err();
466
467 assert!(error.to_string().contains("requires a funder distinct"));
468 }
469
470 #[rstest]
471 #[case(None, "0xsigner")]
472 #[case(Some("0xfunder"), "0xfunder")]
473 fn eoa_uses_configured_funder_or_signer(#[case] funder: Option<&str>, #[case] expected: &str) {
474 let maker_address =
475 resolve_maker_address(PolymarketSignatureType::Eoa, "0xsigner", funder).unwrap();
476
477 assert_eq!(maker_address, expected);
478 }
479}