Skip to main content

polyoxide_clob/api/
markets.rs

1use polyoxide_core::{HttpClient, QueryBuilder};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    request::{AuthMode, Request},
7    types::OrderSide,
8};
9
10/// Markets namespace for market-related operations
11#[derive(Clone)]
12pub struct Markets {
13    pub(crate) http_client: HttpClient,
14    pub(crate) chain_id: u64,
15}
16
17impl Markets {
18    /// Get a market by condition ID
19    pub fn get(&self, condition_id: impl Into<String>) -> Request<Market> {
20        Request::get(
21            self.http_client.clone(),
22            format!("/markets/{}", urlencoding::encode(&condition_id.into())),
23            AuthMode::None,
24            self.chain_id,
25        )
26    }
27
28    pub fn get_by_token_ids(
29        &self,
30        token_ids: impl Into<Vec<String>>,
31    ) -> Request<ListMarketsResponse> {
32        Request::get(
33            self.http_client.clone(),
34            "/markets",
35            AuthMode::None,
36            self.chain_id,
37        )
38        .query_many("clob_token_ids", token_ids.into())
39    }
40
41    /// List all markets
42    pub fn list(&self) -> Request<ListMarketsResponse> {
43        Request::get(
44            self.http_client.clone(),
45            "/markets",
46            AuthMode::None,
47            self.chain_id,
48        )
49    }
50
51    /// Get order book for a token
52    pub fn order_book(&self, token_id: impl Into<String>) -> Request<OrderBook> {
53        Request::get(
54            self.http_client.clone(),
55            "/book",
56            AuthMode::None,
57            self.chain_id,
58        )
59        .query("token_id", token_id.into())
60    }
61
62    /// Get price for a token and side
63    pub fn price(&self, token_id: impl Into<String>, side: OrderSide) -> Request<PriceResponse> {
64        Request::get(
65            self.http_client.clone(),
66            "/price",
67            AuthMode::None,
68            self.chain_id,
69        )
70        .query("token_id", token_id.into())
71        .query("side", side.as_str())
72    }
73
74    /// Get midpoint price for a token
75    pub fn midpoint(&self, token_id: impl Into<String>) -> Request<MidpointResponse> {
76        Request::get(
77            self.http_client.clone(),
78            "/midpoint",
79            AuthMode::None,
80            self.chain_id,
81        )
82        .query("token_id", token_id.into())
83    }
84
85    /// Get historical prices for a token
86    pub fn prices_history(&self, token_id: impl Into<String>) -> Request<PricesHistoryResponse> {
87        Request::get(
88            self.http_client.clone(),
89            "/prices-history",
90            AuthMode::None,
91            self.chain_id,
92        )
93        .query("market", token_id.into())
94    }
95
96    /// Get neg_risk status for a token
97    pub fn neg_risk(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
98        Request::get(
99            self.http_client.clone(),
100            "/neg-risk".to_string(),
101            AuthMode::None,
102            self.chain_id,
103        )
104        .query("token_id", token_id.into())
105    }
106
107    /// Get the current fee rate for a token
108    pub fn fee_rate(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
109        Request::get(
110            self.http_client.clone(),
111            "/fee-rate",
112            AuthMode::None,
113            self.chain_id,
114        )
115        .query("token_id", token_id.into())
116    }
117
118    /// Get tick size for a token
119    pub fn tick_size(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
120        Request::get(
121            self.http_client.clone(),
122            "/tick-size".to_string(),
123            AuthMode::None,
124            self.chain_id,
125        )
126        .query("token_id", token_id.into())
127    }
128}
129
130/// Market information
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Market {
133    pub condition_id: String,
134    pub question_id: String,
135    pub tokens: Vec<MarketToken>,
136    pub rewards: Option<serde_json::Value>,
137    pub minimum_order_size: f64,
138    pub minimum_tick_size: f64,
139    pub description: String,
140    pub category: Option<String>,
141    pub end_date_iso: Option<String>,
142    pub question: String,
143    pub active: bool,
144    pub closed: bool,
145    pub archived: bool,
146    pub neg_risk: Option<bool>,
147    pub neg_risk_market_id: Option<String>,
148    pub enable_order_book: Option<bool>,
149}
150
151/// Markets list response
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ListMarketsResponse {
154    pub data: Vec<Market>,
155    pub next_cursor: Option<String>,
156}
157
158/// Market token (outcome)
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct MarketToken {
161    pub token_id: Option<String>,
162    pub outcome: String,
163    pub price: Option<f64>,
164    pub winner: Option<bool>,
165}
166
167/// Order book level (price and size)
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct OrderLevel {
170    #[serde(with = "rust_decimal::serde::str")]
171    pub price: Decimal,
172    #[serde(with = "rust_decimal::serde::str")]
173    pub size: Decimal,
174}
175
176/// Order book data
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct OrderBook {
179    pub market: String,
180    pub asset_id: String,
181    pub bids: Vec<OrderLevel>,
182    pub asks: Vec<OrderLevel>,
183    pub timestamp: String,
184    pub hash: String,
185}
186
187/// Price response
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct PriceResponse {
190    pub price: String,
191}
192
193/// Midpoint price response
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct MidpointResponse {
196    pub mid: String,
197}
198
199/// A single point in the price history timeseries
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PriceHistoryPoint {
202    /// Unix timestamp (seconds)
203    #[serde(rename = "t")]
204    pub timestamp: i64,
205    /// Price at this point in time
206    #[serde(rename = "p")]
207    pub price: f64,
208}
209
210/// Response from the prices-history endpoint
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct PricesHistoryResponse {
213    pub history: Vec<PriceHistoryPoint>,
214}
215
216/// Response from the neg-risk endpoint
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct NegRiskResponse {
219    pub neg_risk: bool,
220}
221
222/// Response from the fee-rate endpoint
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct FeeRateResponse {
225    pub base_fee: u32,
226}
227
228/// Response from the tick-size endpoint
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct TickSizeResponse {
231    #[serde(deserialize_with = "deserialize_tick_size")]
232    pub minimum_tick_size: String,
233}
234
235fn deserialize_tick_size<'de, D>(deserializer: D) -> Result<String, D::Error>
236where
237    D: serde::Deserializer<'de>,
238{
239    use serde::Deserialize;
240    let v = serde_json::Value::deserialize(deserializer)?;
241    match v {
242        serde_json::Value::String(s) => Ok(s),
243        serde_json::Value::Number(n) => Ok(n.to_string()),
244        _ => Err(serde::de::Error::custom(
245            "expected string or number for tick size",
246        )),
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_fee_rate_response_deserializes() {
256        let json = r#"{"base_fee": 100}"#;
257        let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
258        assert_eq!(resp.base_fee, 100);
259    }
260
261    #[test]
262    fn test_fee_rate_response_deserializes_zero() {
263        let json = r#"{"base_fee": 0}"#;
264        let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
265        assert_eq!(resp.base_fee, 0);
266    }
267
268    #[test]
269    fn test_fee_rate_response_rejects_missing_field() {
270        let json = r#"{"feeRate": "100"}"#;
271        let result = serde_json::from_str::<FeeRateResponse>(json);
272        assert!(result.is_err(), "Should reject JSON missing base_fee field");
273    }
274
275    #[test]
276    fn test_fee_rate_response_rejects_empty_json() {
277        let json = r#"{}"#;
278        let result = serde_json::from_str::<FeeRateResponse>(json);
279        assert!(result.is_err(), "Should reject empty JSON object");
280    }
281}