use std::time::Duration;
use async_trait::async_trait;
use reqwest::Client;
use crate::core::{
AccountType, ExchangeError, ExchangeId, ExchangeResult, ExchangeType,
Kline, OrderBook, Price, Symbol, SymbolInfo, Ticker,
};
use crate::core::traits::{ExchangeIdentity, MarketData};
use super::auth::{PolymarketAuth, PolymarketCredentials};
use super::endpoints::{
PolymarketEndpoints, map_interval, get_fidelity,
};
use super::parser::{
PolymarketParser, ClobMarket, PolyMarket, PolyOrderBook, PolyMidpoint,
PolyEvent,
clob_market_to_symbol_info, clob_market_to_ticker,
price_history_to_klines, poly_orderbook_to_v5,
interval_to_ms,
};
pub struct PolymarketConnector {
client: Client,
_auth: PolymarketAuth,
endpoints: PolymarketEndpoints,
}
impl PolymarketConnector {
pub fn public() -> Self {
Self {
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()
.unwrap_or_default(),
_auth: PolymarketAuth::new(),
endpoints: PolymarketEndpoints::default(),
}
}
pub fn authenticated(creds: PolymarketCredentials) -> Self {
Self {
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()
.unwrap_or_default(),
_auth: PolymarketAuth::with_credentials(creds),
endpoints: PolymarketEndpoints::default(),
}
}
pub fn from_env() -> Self {
Self {
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()
.unwrap_or_default(),
_auth: PolymarketAuth::from_env(),
endpoints: PolymarketEndpoints::default(),
}
}
async fn get_url(&self, url: &str) -> ExchangeResult<serde_json::Value> {
let response = self
.client
.get(url)
.send()
.await
.map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(ExchangeError::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, body.chars().take(200).collect::<String>()),
});
}
let json: serde_json::Value = response
.json()
.await
.map_err(|e| ExchangeError::Parse(format!("JSON parse error: {}", e)))?;
PolymarketParser::check_error(&json)?;
Ok(json)
}
async fn get_clob(
&self,
path: &str,
params: &[(&str, &str)],
) -> ExchangeResult<serde_json::Value> {
let base_url = format!("{}{}", self.endpoints.clob_base, path);
let url = if params.is_empty() {
base_url
} else {
let qs: String = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
format!("{}?{}", base_url, qs)
};
self.get_url(&url).await
}
async fn get_gamma(
&self,
path: &str,
params: &[(&str, &str)],
) -> ExchangeResult<serde_json::Value> {
let base_url = format!("{}{}", self.endpoints.gamma_base, path);
let url = if params.is_empty() {
base_url
} else {
let qs: String = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
format!("{}?{}", base_url, qs)
};
self.get_url(&url).await
}
pub async fn get_markets(
&self,
limit: Option<u32>,
next_cursor: Option<&str>,
) -> ExchangeResult<(Vec<ClobMarket>, Option<String>)> {
let limit_str;
let cursor_str;
let mut params: Vec<(&str, &str)> = Vec::new();
if let Some(l) = limit {
limit_str = l.to_string();
params.push(("limit", &limit_str));
}
if let Some(c) = next_cursor {
cursor_str = c.to_string();
params.push(("next_cursor", &cursor_str));
}
let response = self.get_clob("/markets", ¶ms).await?;
let markets = PolymarketParser::parse_clob_markets(&response)?;
let next = PolymarketParser::get_next_cursor(&response);
Ok((markets, next))
}
pub async fn get_market(&self, condition_id: &str) -> ExchangeResult<ClobMarket> {
let url = format!("{}/markets/{}", self.endpoints.clob_base, condition_id);
let response = self.get_url(&url).await?;
PolymarketParser::parse_clob_market(&response)
}
pub async fn get_order_book(&self, token_id: &str) -> ExchangeResult<PolyOrderBook> {
let response = self.get_clob("/book", &[("token_id", token_id)]).await?;
PolymarketParser::parse_order_book(&response)
}
pub async fn get_midpoint(&self, token_id: &str) -> ExchangeResult<PolyMidpoint> {
let response = self.get_clob("/midpoint", &[("token_id", token_id)]).await?;
PolymarketParser::parse_midpoint(&response)
}
pub async fn get_last_trade_price(&self, token_id: &str) -> ExchangeResult<f64> {
let response = self.get_clob("/last-trade-price", &[("token_id", token_id)]).await?;
PolymarketParser::parse_price(&response)
}
pub async fn get_active_markets(&self, limit: Option<u32>) -> ExchangeResult<Vec<ClobMarket>> {
let limit_str;
let mut params: Vec<(&str, &str)> = vec![("active", "true")];
if let Some(l) = limit {
limit_str = l.to_string();
params.push(("limit", &limit_str));
}
let response = self.get_clob("/markets", ¶ms).await?;
PolymarketParser::parse_clob_markets(&response)
}
pub async fn get_events(&self, limit: Option<u32>) -> ExchangeResult<Vec<PolyEvent>> {
let limit_str;
let mut params: Vec<(&str, &str)> = Vec::new();
if let Some(l) = limit {
limit_str = l.to_string();
params.push(("limit", &limit_str));
}
let response = self.get_gamma("/events", ¶ms).await?;
PolymarketParser::parse_events(&response)
}
pub async fn get_event(&self, event_id: &str) -> ExchangeResult<PolyEvent> {
let url = format!("{}/events/{}", self.endpoints.gamma_base, event_id);
let response = self.get_url(&url).await?;
PolymarketParser::parse_event(&response)
}
pub async fn get_gamma_markets(&self, limit: Option<u32>) -> ExchangeResult<Vec<PolyMarket>> {
let limit_str;
let mut params: Vec<(&str, &str)> = Vec::new();
if let Some(l) = limit {
limit_str = l.to_string();
params.push(("limit", &limit_str));
}
let response = self.get_gamma("/markets", ¶ms).await?;
PolymarketParser::parse_gamma_markets(&response)
}
async fn get_yes_token_id(&self, condition_id: &str) -> ExchangeResult<String> {
let market = self.get_market(condition_id).await?;
market
.tokens
.iter()
.find(|t| t.outcome == "Yes")
.map(|t| t.token_id.clone())
.ok_or_else(|| {
ExchangeError::Parse(format!(
"No YES token found for condition_id {}",
condition_id
))
})
}
}
impl Default for PolymarketConnector {
fn default() -> Self {
Self::public()
}
}
impl ExchangeIdentity for PolymarketConnector {
fn exchange_id(&self) -> ExchangeId {
ExchangeId::Polymarket
}
fn is_testnet(&self) -> bool {
false
}
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
fn exchange_type(&self) -> ExchangeType {
ExchangeType::DataProvider
}
}
#[async_trait]
impl MarketData for PolymarketConnector {
async fn ping(&self) -> ExchangeResult<()> {
self.get_clob("/time", &[]).await?;
Ok(())
}
async fn get_price(
&self,
symbol: Symbol,
_account_type: AccountType,
) -> ExchangeResult<Price> {
let condition_id = symbol.raw().unwrap_or(&symbol.base);
let market = self.get_market(condition_id).await?;
let yes_token = market
.tokens
.iter()
.find(|t| t.outcome == "Yes")
.ok_or_else(|| {
ExchangeError::Parse(format!(
"No YES token found for market {}",
condition_id
))
})?;
match self.get_last_trade_price(&yes_token.token_id).await {
Ok(price) => Ok(price),
Err(_) => {
let midpoint = self.get_midpoint(&yes_token.token_id).await?;
Ok(midpoint.mid)
}
}
}
async fn get_orderbook(
&self,
symbol: Symbol,
_depth: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
let condition_id = symbol.raw().unwrap_or(&symbol.base);
let yes_token_id = self.get_yes_token_id(condition_id).await?;
let poly_book = self.get_order_book(&yes_token_id).await?;
Ok(poly_orderbook_to_v5(&poly_book))
}
async fn get_klines(
&self,
symbol: Symbol,
interval: &str,
limit: Option<u16>,
_account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
let input = symbol.raw().unwrap_or(&symbol.base);
let token_id = if input.starts_with("0x") && input.len() == 66 {
self.get_yes_token_id(input).await?
} else {
input.to_string()
};
let poly_interval = map_interval(interval);
let fidelity = get_fidelity(limit);
let fidelity_str = fidelity.to_string();
let response = self
.get_clob(
"/prices-history",
&[
("market", token_id.as_str()),
("interval", poly_interval),
("fidelity", fidelity_str.as_str()),
],
)
.await?;
let history = PolymarketParser::parse_price_history(&response)?;
let interval_ms = interval_to_ms(poly_interval);
let klines = price_history_to_klines(history, interval_ms);
Ok(klines)
}
async fn get_ticker(
&self,
symbol: Symbol,
_account_type: AccountType,
) -> ExchangeResult<Ticker> {
let condition_id = symbol.raw().unwrap_or(&symbol.base);
let market = self.get_market(condition_id).await?;
clob_market_to_ticker(&market).ok_or_else(|| {
ExchangeError::Parse(format!(
"No ticker data available for market {}",
condition_id
))
})
}
async fn get_exchange_info(
&self,
account_type: AccountType,
) -> ExchangeResult<Vec<SymbolInfo>> {
let markets = self.get_active_markets(Some(500)).await?;
let symbols = markets
.iter()
.map(|m| clob_market_to_symbol_info(m, account_type))
.collect();
Ok(symbols)
}
}