Skip to main content

nautilus_polymarket/execution/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Live execution client implementation for the Polymarket adapter.
17
18pub 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/// Live execution client for the Polymarket prediction market.
90#[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    /// Creates a new [`PolymarketExecutionClient`].
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if credentials cannot be resolved or clients fail to construct.
124    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}