1mod cancellations;
19mod lifecycle;
20mod orders;
21mod reports;
22mod responses;
23
24pub(crate) mod identity;
25pub mod order_builder;
26pub(crate) mod order_fill_tracker;
27pub mod parse;
28pub(crate) mod pending;
29pub(crate) mod reconciliation;
30pub(crate) mod submitter;
31pub(crate) mod types;
32
33use std::sync::{Arc, Mutex, atomic::AtomicBool};
34
35use anyhow::Context;
36use async_trait::async_trait;
37use nautilus_common::{
38 clients::ExecutionClient,
39 messages::execution::{
40 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
41 GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
42 ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
43 },
44 msgbus::TypedHandler,
45};
46use nautilus_core::{
47 UnixNanos,
48 collections::AtomicMap,
49 time::{AtomicTime, get_atomic_clock_realtime},
50};
51use nautilus_live::{ExecutionClientCore, ExecutionEventEmitter};
52use nautilus_model::{
53 accounts::AccountAny,
54 enums::{AccountType, LiquiditySide, OmsType},
55 events::{OrderEventAny, PositionEvent},
56 identifiers::{
57 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
58 },
59 instruments::InstrumentAny,
60 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
61 types::{AccountBalance, MarginBalance, Money, Price, Quantity},
62};
63use nautilus_network::retry::RetryConfig;
64use tokio::task::JoinHandle;
65use ustr::Ustr;
66
67pub(crate) use self::reports::get_pusd_currency;
68use self::{
69 identity::OrderIdentityRegistry,
70 order_builder::PolymarketOrderBuilder,
71 order_fill_tracker::OrderFillTrackerMap,
72 pending::{PendingCancelTracker, PendingSubmitTracker},
73 submitter::OrderSubmitter,
74};
75use crate::{
76 common::{consts::POLYMARKET_VENUE, credential::Secrets, enums::SignatureType},
77 config::PolymarketExecClientConfig,
78 http::{clob::PolymarketClobHttpClient, data_api::PolymarketDataApiHttpClient},
79 signing::eip712::OrderSigner,
80 websocket::client::PolymarketWebSocketClient,
81};
82
83#[derive(Debug)]
85pub struct PolymarketExecutionClient {
86 core: ExecutionClientCore,
87 clock: &'static AtomicTime,
88 config: PolymarketExecClientConfig,
89 emitter: ExecutionEventEmitter,
90 http_client: PolymarketClobHttpClient,
91 data_api_client: PolymarketDataApiHttpClient,
92 submitter: OrderSubmitter,
93 ws_client: PolymarketWebSocketClient,
94 secrets: Secrets,
95 pending_tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
96 stopping: Arc<AtomicBool>,
97 ws_stream_handle: Mutex<Option<JoinHandle<()>>>,
98 order_event_handler: Option<TypedHandler<OrderEventAny>>,
99 position_event_handler: Option<TypedHandler<PositionEvent>>,
100 shared_token_instruments: Arc<AtomicMap<Ustr, InstrumentAny>>,
101 neg_risk_index: Arc<AtomicMap<InstrumentId, bool>>,
102 pending_submits: PendingSubmitTracker,
103 pending_cancels: PendingCancelTracker,
104 order_identities: Arc<OrderIdentityRegistry>,
105 fill_tracker: Arc<OrderFillTrackerMap>,
106}
107
108impl PolymarketExecutionClient {
109 pub fn new(
115 core: ExecutionClientCore,
116 config: PolymarketExecClientConfig,
117 ) -> anyhow::Result<Self> {
118 let secrets = Secrets::resolve(
119 config.private_key.as_deref(),
120 config.api_key.clone(),
121 config.api_secret.clone(),
122 config.passphrase.clone(),
123 config.funder.clone(),
124 )
125 .context("failed to resolve Polymarket credentials")?;
126
127 let signer_address = secrets.address.clone();
128 let maker_address = secrets
129 .funder
130 .clone()
131 .unwrap_or_else(|| signer_address.clone());
132 if config.signature_type == SignatureType::Poly1271
133 && maker_address.eq_ignore_ascii_case(&signer_address)
134 {
135 anyhow::bail!(
136 "POLY_1271 signature type requires a deposit wallet funder distinct from the signing address"
137 );
138 }
139 let http_client = PolymarketClobHttpClient::new(
140 secrets.credential.clone(),
141 signer_address.clone(),
142 config.base_url_http.clone(),
143 config.http_timeout_secs,
144 )
145 .map_err(|e| anyhow::anyhow!("{e}"))
146 .context("failed to create CLOB HTTP client")?;
147
148 let data_api_client =
149 PolymarketDataApiHttpClient::new(Some(config.data_api_url()), config.http_timeout_secs)
150 .map_err(|e| anyhow::anyhow!("{e}"))
151 .context("failed to create Data API HTTP client")?;
152
153 let order_signer =
154 OrderSigner::new(&secrets.private_key).context("failed to create order signer")?;
155 let order_builder = Arc::new(PolymarketOrderBuilder::new(
156 order_signer,
157 signer_address,
158 maker_address,
159 config.signature_type,
160 ));
161
162 let retry_config = RetryConfig {
163 max_retries: config.max_retries,
164 initial_delay_ms: config.retry_delay_initial_ms,
165 max_delay_ms: config.retry_delay_max_ms,
166 backoff_factor: 2.0,
167 jitter_ms: 1_000,
168 operation_timeout_ms: Some(config.http_timeout_secs * 1_000),
169 immediate_first: false,
170 max_elapsed_ms: Some(180_000),
171 };
172 let submitter = OrderSubmitter::new(http_client.clone(), order_builder, retry_config);
173
174 let ws_client = PolymarketWebSocketClient::new_user(
175 config.base_url_ws.clone(),
176 secrets.credential.clone(),
177 config.transport_backend,
178 );
179
180 let clock = get_atomic_clock_realtime();
181 let pusd = get_pusd_currency();
182 let emitter = ExecutionEventEmitter::new(
183 clock,
184 core.trader_id,
185 core.account_id,
186 AccountType::Cash,
187 Some(pusd),
188 );
189
190 Ok(Self {
191 core,
192 clock,
193 config,
194 emitter,
195 http_client,
196 data_api_client,
197 submitter,
198 ws_client,
199 secrets,
200 pending_tasks: Arc::new(Mutex::new(Vec::new())),
201 stopping: Arc::new(AtomicBool::new(false)),
202 ws_stream_handle: Mutex::new(None),
203 order_event_handler: None,
204 position_event_handler: None,
205 shared_token_instruments: Arc::new(AtomicMap::new()),
206 neg_risk_index: Arc::new(AtomicMap::new()),
207 pending_submits: PendingSubmitTracker::default(),
208 pending_cancels: PendingCancelTracker::default(),
209 order_identities: Arc::new(OrderIdentityRegistry::default()),
210 fill_tracker: Arc::new(OrderFillTrackerMap::new()),
211 })
212 }
213}
214
215#[async_trait(?Send)]
216impl ExecutionClient for PolymarketExecutionClient {
217 fn is_connected(&self) -> bool {
218 self.core.is_connected()
219 }
220
221 fn client_id(&self) -> ClientId {
222 self.core.client_id
223 }
224
225 fn account_id(&self) -> AccountId {
226 self.core.account_id
227 }
228
229 fn venue(&self) -> Venue {
230 *POLYMARKET_VENUE
231 }
232
233 fn oms_type(&self) -> OmsType {
234 OmsType::Netting
235 }
236
237 fn get_account(&self) -> Option<AccountAny> {
238 self.core.cache().account_owned(&self.core.account_id)
239 }
240
241 fn generate_account_state(
242 &self,
243 balances: Vec<AccountBalance>,
244 margins: Vec<MarginBalance>,
245 reported: bool,
246 ts_event: UnixNanos,
247 ) -> anyhow::Result<()> {
248 self.emitter
249 .emit_account_state(balances, margins, reported, ts_event);
250 Ok(())
251 }
252
253 fn start(&mut self) -> anyhow::Result<()> {
254 self.start_client();
255 Ok(())
256 }
257
258 fn stop(&mut self) -> anyhow::Result<()> {
259 self.stop_client();
260 Ok(())
261 }
262
263 fn reset(&mut self) -> anyhow::Result<()> {
264 self.reset_client();
265 Ok(())
266 }
267
268 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
269 self.submit_order_command(&cmd)
270 }
271
272 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
273 self.submit_order_list_command(&cmd);
274 Ok(())
275 }
276
277 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
278 self.modify_order_command(&cmd);
279 Ok(())
280 }
281
282 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
283 self.cancel_order_command(&cmd);
284 Ok(())
285 }
286
287 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
288 self.cancel_all_orders_command(&cmd);
289 Ok(())
290 }
291
292 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
293 self.batch_cancel_orders_command(&cmd);
294 Ok(())
295 }
296
297 fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
298 self.query_account_command(cmd);
299 Ok(())
300 }
301
302 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
303 self.query_order_command(&cmd);
304 Ok(())
305 }
306
307 fn register_external_order(
308 &self,
309 _client_order_id: ClientOrderId,
310 _venue_order_id: VenueOrderId,
311 _instrument_id: InstrumentId,
312 _strategy_id: StrategyId,
313 _ts_init: UnixNanos,
314 ) {
315 }
316
317 fn on_instrument(&mut self, instrument: InstrumentAny) {
318 self.on_instrument_update(&instrument);
319 }
320
321 fn calculate_commission(
322 &self,
323 instrument: &InstrumentAny,
324 last_qty: Quantity,
325 last_px: Price,
326 liquidity_side: LiquiditySide,
327 ) -> Option<Money> {
328 Some(self.calculate_commission_impl(instrument, last_qty, last_px, liquidity_side))
329 }
330
331 async fn connect(&mut self) -> anyhow::Result<()> {
332 self.connect_client().await
333 }
334
335 async fn disconnect(&mut self) -> anyhow::Result<()> {
336 self.disconnect_client().await
337 }
338
339 async fn generate_order_status_report(
340 &self,
341 cmd: &GenerateOrderStatusReport,
342 ) -> anyhow::Result<Option<OrderStatusReport>> {
343 self.generate_order_status_report_impl(cmd).await
344 }
345
346 async fn generate_order_status_reports(
347 &self,
348 cmd: &GenerateOrderStatusReports,
349 ) -> anyhow::Result<Vec<OrderStatusReport>> {
350 self.generate_order_status_reports_impl(cmd).await
351 }
352
353 async fn generate_fill_reports(
354 &self,
355 cmd: GenerateFillReports,
356 ) -> anyhow::Result<Vec<FillReport>> {
357 self.generate_fill_reports_impl(cmd).await
358 }
359
360 async fn generate_position_status_reports(
361 &self,
362 cmd: &GeneratePositionStatusReports,
363 ) -> anyhow::Result<Vec<PositionStatusReport>> {
364 self.generate_position_status_reports_impl(cmd).await
365 }
366
367 async fn generate_mass_status(
368 &self,
369 lookback_mins: Option<u64>,
370 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
371 self.generate_mass_status_impl(lookback_mins).await
372 }
373}