Skip to main content

limitless/
trading.rs

1use crate::prelude::*;
2use crate::signing::Eip712Signer;
3
4/// Manages trading operations: order creation, cancellation, status lookup,
5/// orderbook access, historical prices, and user-specific order/market data.
6///
7/// Most endpoints require authentication via scoped HMAC token or legacy API key.
8///
9/// # Convenience Methods
10///
11/// For the most common trading workflows, use the high-level methods:
12///
13/// - [`buy_gtc`](Trader::buy_gtc) / [`sell_gtc`](Trader::sell_gtc) — Place limit orders
14/// - [`buy_fok`](Trader::buy_fok) / [`sell_fok`](Trader::sell_fok) — Place market orders
15/// - [`cancel_all`](Trader::cancel_all) — Cancel all orders in a market
16#[derive(Clone)]
17pub struct Trader {
18    pub client: Client,
19}
20
21impl Trader {
22    /// Create a new order on a prediction market.
23    ///
24    /// Supports GTC (Good Till Cancelled) and FOK (Fill or Kill) order types.
25    /// CLOB orders require EIP-712 signatures; AMM orders use a different flow.
26    ///
27    /// # Arguments
28    ///
29    /// * `order_request` — Serialized JSON body matching the Create Order schema.
30    pub async fn create_order(
31        &self,
32        order_request: &str,
33    ) -> Result<CreateOrderResponse, LimitlessError> {
34        self.client
35            .post_signed("orders", Some(order_request.to_string()))
36            .await
37    }
38
39    /// Fetch statuses for multiple orders in batch.
40    ///
41    /// Look up by `orderId` or `clientOrderId` (provide exactly one per item).
42    /// Accepts up to 50 items per request.
43    pub async fn order_status_batch(
44        &self,
45        request_body: &str,
46    ) -> Result<OrderStatusBatchResponse, LimitlessError> {
47        self.client
48            .post_signed("orders/status/batch", Some(request_body.to_string()))
49            .await
50    }
51
52    /// Cancel a single order by `orderId` or `clientOrderId` (combined endpoint).
53    pub async fn cancel_combined(
54        &self,
55        request_body: &str,
56    ) -> Result<CancelOrderResponse, LimitlessError> {
57        self.client
58            .post_signed("orders/cancel", Some(request_body.to_string()))
59            .await
60    }
61
62    /// Cancel multiple orders by internal `orderId`s (batch).
63    pub async fn cancel_batch(
64        &self,
65        request_body: &str,
66    ) -> Result<CancelBatchResponse, LimitlessError> {
67        self.client
68            .post_signed("orders/cancel-batch", Some(request_body.to_string()))
69            .await
70    }
71
72    /// Cancel a single order by internal `orderId` (legacy endpoint).
73    pub async fn cancel_order_by_id(
74        &self,
75        order_id: &str,
76    ) -> Result<CancelOrderResponse, LimitlessError> {
77        let path = format!("orders/{}", order_id);
78        self.client.delete_signed(&path).await
79    }
80
81    /// Cancel all orders for the authenticated user in a specific market.
82    pub async fn cancel_all_in_market(
83        &self,
84        slug: &str,
85    ) -> Result<CancelAllResponse, LimitlessError> {
86        let path = format!("orders/all/{}", slug);
87        self.client.delete_signed(&path).await
88    }
89
90    /// Get the current orderbook for a market.
91    pub async fn get_orderbook(&self, slug: &str) -> Result<OrderbookResponse, LimitlessError> {
92        let path = format!("markets/{}/orderbook", slug);
93        self.client.get(&path, None).await
94    }
95
96    /// Get historical price data for a market.
97    pub async fn get_historical_prices(
98        &self,
99        slug: &str,
100        interval: Option<&str>,
101    ) -> Result<Vec<HistoricalPriceData>, LimitlessError> {
102        let mut params = BTreeMap::new();
103        if let Some(ref v) = interval {
104            params.insert("interval".into(), v.to_string());
105        }
106        let request = build_request(&params);
107        let path = format!("markets/{}/historical-price", slug);
108        self.client.get(&path, Some(request)).await
109    }
110
111    /// Get the amount of funds locked in open orders for the authenticated user.
112    pub async fn get_locked_balance(
113        &self,
114        slug: &str,
115    ) -> Result<LockedBalanceResponse, LimitlessError> {
116        let path = format!("markets/{}/locked-balance", slug);
117        self.client.get(&path, None).await
118    }
119
120    /// Get all orders placed by the authenticated user for a specific market.
121    pub async fn get_user_orders(
122        &self,
123        slug: &str,
124        statuses: Option<&[&str]>,
125        limit: Option<u64>,
126    ) -> Result<UserOrdersResponse, LimitlessError> {
127        let mut params = BTreeMap::new();
128        if let Some(s) = statuses {
129            params.insert("statuses".into(), s.join(","));
130        }
131        if let Some(v) = limit {
132            params.insert("limit".into(), v.to_string());
133        }
134        let request = build_request(&params);
135        let path = format!("markets/{}/user-orders", slug);
136        self.client.get(&path, Some(request)).await
137    }
138
139    /// Get recent market events (trades, orders, liquidity changes).
140    pub async fn get_market_events(
141        &self,
142        slug: &str,
143        page: Option<u64>,
144        limit: Option<u64>,
145    ) -> Result<MarketEventsResponse, LimitlessError> {
146        let mut params = BTreeMap::new();
147        if let Some(v) = page {
148            params.insert("page".into(), v.to_string());
149        }
150        if let Some(v) = limit {
151            params.insert("limit".into(), v.to_string());
152        }
153        let request = build_request(&params);
154        let path = format!("markets/{}/events", slug);
155        self.client.get(&path, Some(request)).await
156    }
157
158    // ── High-level convenience methods ──────────────────────────────────
159
160    /// Place a GTC buy limit order — the simplest way to buy YES/NO shares.
161    ///
162    /// Handles: fetch venue contract → validate → build EIP-712 order → sign → submit.
163    ///
164    /// # Arguments
165    /// * `private_key` — 0x-prefixed hex private key for signing
166    /// * `market_slug` — Market identifier (e.g., "btc-above-100k-jul-4")
167    /// * `token_id` — The outcome token ID as a decimal string (e.g., from `market.outcomes[0].token_id`)
168    /// * `price` — Price between 0 and 1 (e.g., 0.55 for $0.55)
169    /// * `size` — Number of shares to buy
170    /// * `owner_id` — Your profile ID (from `GET /profiles/:address`)
171    pub async fn buy_gtc(
172        &self,
173        private_key: &str,
174        market_slug: &str,
175        token_id: &str,
176        price: f64,
177        size: f64,
178        owner_id: u64,
179    ) -> Result<CreateOrderResponse, LimitlessError> {
180        self.place_gtc_order(
181            private_key,
182            market_slug,
183            token_id,
184            OrderSide::Buy,
185            price,
186            size,
187            owner_id,
188        )
189        .await
190    }
191
192    /// Place a GTC sell limit order — the simplest way to sell YES/NO shares.
193    pub async fn sell_gtc(
194        &self,
195        private_key: &str,
196        market_slug: &str,
197        token_id: &str,
198        price: f64,
199        size: f64,
200        owner_id: u64,
201    ) -> Result<CreateOrderResponse, LimitlessError> {
202        self.place_gtc_order(
203            private_key,
204            market_slug,
205            token_id,
206            OrderSide::Sell,
207            price,
208            size,
209            owner_id,
210        )
211        .await
212    }
213
214    /// Place a FOK buy market order — buy shares at market price.
215    pub async fn buy_fok(
216        &self,
217        private_key: &str,
218        market_slug: &str,
219        token_id: &str,
220        usdc_amount: f64,
221        owner_id: u64,
222    ) -> Result<CreateOrderResponse, LimitlessError> {
223        self.place_fok_order(
224            private_key,
225            market_slug,
226            token_id,
227            OrderSide::Buy,
228            usdc_amount,
229            owner_id,
230        )
231        .await
232    }
233
234    /// Place a FOK sell market order — sell shares at market price.
235    pub async fn sell_fok(
236        &self,
237        private_key: &str,
238        market_slug: &str,
239        token_id: &str,
240        share_amount: f64,
241        owner_id: u64,
242    ) -> Result<CreateOrderResponse, LimitlessError> {
243        self.place_fok_order(
244            private_key,
245            market_slug,
246            token_id,
247            OrderSide::Sell,
248            share_amount,
249            owner_id,
250        )
251        .await
252    }
253
254    /// Cancel all open orders in a market (convenience alias).
255    pub async fn cancel_all(&self, slug: &str) -> Result<CancelAllResponse, LimitlessError> {
256        self.cancel_all_in_market(slug).await
257    }
258
259    // ── Internal helpers ───────────────────────────────────────────────
260
261    /// Resolve the verifying contract for a market slug by fetching market details.
262    async fn get_verifying_contract(&self, slug: &str) -> Result<String, LimitlessError> {
263        let market: MarketDetail = self.client.get(&format!("markets/{}", slug), None).await?;
264        let venue = market.venue.ok_or_else(|| {
265            LimitlessError::ValidationError(format!(
266                "Market '{}' has no venue info — is it a CLOB market?",
267                slug
268            ))
269        })?;
270        Ok(venue.exchange)
271    }
272
273    async fn place_gtc_order(
274        &self,
275        private_key: &str,
276        market_slug: &str,
277        token_id: &str,
278        side: OrderSide,
279        price: f64,
280        size: f64,
281        owner_id: u64,
282    ) -> Result<CreateOrderResponse, LimitlessError> {
283        let verifying_contract = self.get_verifying_contract(market_slug).await?;
284        let signer = Eip712Signer::new(private_key, &verifying_contract)
285            .map_err(|e| LimitlessError::ValidationError(e))?;
286
287        let order_data = signer
288            .build_gtc_order(
289                &signer.wallet_address(),
290                token_id,
291                side,
292                price,
293                size,
294                0, // fee_rate_bps — use default
295            )
296            .map_err(|e| LimitlessError::ValidationError(e))?;
297
298        let request = CreateOrderRequest {
299            order: order_data,
300            owner_id,
301            order_type: OrderType::Gtc,
302            market_slug: market_slug.to_string(),
303            client_order_id: None,
304            on_behalf_of: None,
305        };
306
307        let body = serde_json::to_string(&request).map_err(|e| LimitlessError::Json(e))?;
308
309        self.create_order(&body).await
310    }
311
312    async fn place_fok_order(
313        &self,
314        private_key: &str,
315        market_slug: &str,
316        token_id: &str,
317        side: OrderSide,
318        amount: f64,
319        owner_id: u64,
320    ) -> Result<CreateOrderResponse, LimitlessError> {
321        let verifying_contract = self.get_verifying_contract(market_slug).await?;
322        let signer = Eip712Signer::new(private_key, &verifying_contract)
323            .map_err(|e| LimitlessError::ValidationError(e))?;
324
325        let order_data = signer
326            .build_fok_order(&signer.wallet_address(), token_id, side, amount, 0)
327            .map_err(|e| LimitlessError::ValidationError(e))?;
328
329        let request = CreateOrderRequest {
330            order: order_data,
331            owner_id,
332            order_type: OrderType::Fok,
333            market_slug: market_slug.to_string(),
334            client_order_id: None,
335            on_behalf_of: None,
336        };
337
338        let body = serde_json::to_string(&request).map_err(|e| LimitlessError::Json(e))?;
339
340        self.create_order(&body).await
341    }
342}
343
344impl Limitless for Trader {
345    fn new(api_key: Option<String>, secret: Option<String>) -> Self {
346        Self::new_with_config(&Config::default(), api_key, secret)
347    }
348
349    fn new_with_config(config: &Config, api_key: Option<String>, secret: Option<String>) -> Self {
350        Self {
351            client: Client::new(api_key, secret, config.rest_api_endpoint.to_string()),
352        }
353    }
354}