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
18mod 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/// Live execution client for the Polymarket prediction market.
84#[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    /// Creates a new [`PolymarketExecutionClient`].
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if credentials cannot be resolved or clients fail to construct.
114    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}