Skip to main content

px_core/exchange/
traits.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5use crate::error::OpenPxError;
6use crate::models::{
7    Candlestick, Fill, Market, MarketTrade, Order, OrderSide, Orderbook, OrderbookSnapshot,
8    Position, PriceHistoryInterval,
9};
10
11use super::config::{FetchMarketsParams, FetchOrdersParams, FetchUserActivityParams};
12use super::manifest::ExchangeManifest;
13
14#[allow(async_fn_in_trait)]
15pub trait Exchange: Send + Sync {
16    fn id(&self) -> &'static str;
17    fn name(&self) -> &'static str;
18
19    /// Fetch one page of markets from this exchange.
20    ///
21    /// Returns `(markets, next_cursor)` where `next_cursor` is `None` when no more pages.
22    /// Callers paginate externally by passing `next_cursor` back in `params.cursor`.
23    /// When `params.status` is `None`, exchanges default to `Active`.
24    /// When `params.status` is `Some(All)`, exchanges return all markets regardless of status.
25    async fn fetch_markets(
26        &self,
27        params: &FetchMarketsParams,
28    ) -> Result<(Vec<Market>, Option<String>), OpenPxError>;
29
30    async fn fetch_market(&self, market_id: &str) -> Result<Market, OpenPxError>;
31
32    async fn create_order(
33        &self,
34        market_id: &str,
35        outcome: &str,
36        side: OrderSide,
37        price: f64,
38        size: f64,
39        params: HashMap<String, String>,
40    ) -> Result<Order, OpenPxError>;
41
42    async fn cancel_order(
43        &self,
44        order_id: &str,
45        market_id: Option<&str>,
46    ) -> Result<Order, OpenPxError>;
47
48    async fn fetch_order(
49        &self,
50        order_id: &str,
51        market_id: Option<&str>,
52    ) -> Result<Order, OpenPxError>;
53
54    async fn fetch_open_orders(
55        &self,
56        params: Option<FetchOrdersParams>,
57    ) -> Result<Vec<Order>, OpenPxError>;
58
59    async fn fetch_positions(&self, market_id: Option<&str>) -> Result<Vec<Position>, OpenPxError>;
60
61    async fn fetch_balance(&self) -> Result<HashMap<String, f64>, OpenPxError>;
62
63    /// Refresh cached balance/allowance state if supported by the exchange.
64    async fn refresh_balance(&self) -> Result<(), OpenPxError> {
65        Ok(())
66    }
67
68    /// Fetch L2 orderbook for a market outcome.
69    /// Uses owned types for async compatibility.
70    async fn fetch_orderbook(&self, req: OrderbookRequest) -> Result<Orderbook, OpenPxError> {
71        let _ = req;
72        Err(OpenPxError::Exchange(
73            crate::error::ExchangeError::NotSupported("fetch_orderbook".into()),
74        ))
75    }
76
77    /// Fetch historical OHLCV price history / candlestick data for a market outcome.
78    async fn fetch_price_history(
79        &self,
80        req: PriceHistoryRequest,
81    ) -> Result<Vec<Candlestick>, OpenPxError> {
82        let _ = req;
83        Err(OpenPxError::Exchange(
84            crate::error::ExchangeError::NotSupported("fetch_price_history".into()),
85        ))
86    }
87
88    /// Fetch recent public trades ("tape") for a market outcome.
89    /// Returns `(trades, next_cursor)` where `next_cursor` supports pagination.
90    async fn fetch_trades(
91        &self,
92        req: TradesRequest,
93    ) -> Result<(Vec<MarketTrade>, Option<String>), OpenPxError> {
94        let _ = req;
95        Err(OpenPxError::Exchange(
96            crate::error::ExchangeError::NotSupported("fetch_trades".into()),
97        ))
98    }
99
100    /// Fetch historical L2 orderbook snapshots for a market.
101    /// Returns `(snapshots, next_cursor)` for pagination.
102    async fn fetch_orderbook_history(
103        &self,
104        req: OrderbookHistoryRequest,
105    ) -> Result<(Vec<OrderbookSnapshot>, Option<String>), OpenPxError> {
106        let _ = req;
107        Err(OpenPxError::Exchange(
108            crate::error::ExchangeError::NotSupported("fetch_orderbook_history".into()),
109        ))
110    }
111
112    /// Fetch raw balance response from exchange API
113    async fn fetch_balance_raw(&self) -> Result<Value, OpenPxError> {
114        Err(OpenPxError::Exchange(
115            crate::error::ExchangeError::NotSupported("fetch_balance_raw".into()),
116        ))
117    }
118
119    /// Fetch user activity (positions, trades, portfolio data) for a given address.
120    // TODO(trade-history): Implement per-exchange. No exchange currently implements this.
121    // Kalshi: GET /portfolio/fills returns user's fill history with fees, timestamps, maker/taker.
122    // Polymarket: activity API provides user trade history.
123    // Wire this up to a /api/v1/fills or /api/v1/trade-history endpoint and surface
124    // in the terminal UI as a "My Fills" / "Trade History" view.
125    async fn fetch_user_activity(
126        &self,
127        params: FetchUserActivityParams,
128    ) -> Result<Value, OpenPxError> {
129        let _ = params;
130        Err(OpenPxError::Exchange(
131            crate::error::ExchangeError::NotSupported("fetch_user_activity".into()),
132        ))
133    }
134
135    /// Fetch user's fill (trade execution) history for a market.
136    async fn fetch_fills(
137        &self,
138        market_id: Option<&str>,
139        limit: Option<usize>,
140    ) -> Result<Vec<Fill>, OpenPxError> {
141        let _ = (market_id, limit);
142        Err(OpenPxError::Exchange(
143            crate::error::ExchangeError::NotSupported("fetch_fills".into()),
144        ))
145    }
146
147    fn describe(&self) -> ExchangeInfo {
148        ExchangeInfo {
149            id: self.id(),
150            name: self.name(),
151            has_fetch_markets: true,
152            has_create_order: true,
153            has_cancel_order: true,
154            has_fetch_positions: true,
155            has_fetch_balance: true,
156            has_fetch_orderbook: false,
157            has_fetch_price_history: false,
158            has_fetch_trades: false,
159            has_fetch_user_activity: false,
160            has_fetch_fills: false,
161            has_approvals: false,
162            has_refresh_balance: false,
163            has_websocket: false,
164            has_fetch_orderbook_history: false,
165        }
166    }
167
168    /// Returns the exchange manifest containing connection and data mapping configuration.
169    fn manifest(&self) -> &'static ExchangeManifest;
170}
171
172#[derive(Debug, Clone, Serialize)]
173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
174pub struct ExchangeInfo {
175    pub id: &'static str,
176    pub name: &'static str,
177    pub has_fetch_markets: bool,
178    pub has_create_order: bool,
179    pub has_cancel_order: bool,
180    pub has_fetch_positions: bool,
181    pub has_fetch_balance: bool,
182    pub has_fetch_orderbook: bool,
183    pub has_fetch_price_history: bool,
184    pub has_fetch_trades: bool,
185    pub has_fetch_user_activity: bool,
186    pub has_fetch_fills: bool,
187    pub has_approvals: bool,
188    pub has_refresh_balance: bool,
189    pub has_websocket: bool,
190    pub has_fetch_orderbook_history: bool,
191}
192
193/// Request for fetching an L2 orderbook.
194#[derive(Debug, Clone, Default, Serialize, Deserialize)]
195#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
196pub struct OrderbookRequest {
197    pub market_id: String,
198    pub outcome: Option<String>,
199    pub token_id: Option<String>,
200}
201
202/// Request for fetching price history / candlestick data.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
205pub struct PriceHistoryRequest {
206    pub market_id: String,
207    pub outcome: Option<String>,
208    pub token_id: Option<String>,
209    /// Condition ID for OI enrichment (Polymarket).
210    pub condition_id: Option<String>,
211    pub interval: PriceHistoryInterval,
212    /// Unix seconds
213    pub start_ts: Option<i64>,
214    /// Unix seconds
215    pub end_ts: Option<i64>,
216}
217
218/// Request for fetching recent public trades ("tape") for a market outcome.
219#[derive(Debug, Clone, Default, Serialize, Deserialize)]
220#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
221pub struct TradesRequest {
222    /// Exchange-native market identifier.
223    pub market_id: String,
224    /// Optional alternate market identifier for trade endpoints (e.g., Polymarket conditionId).
225    /// When provided, exchanges should prefer this over `market_id`.
226    pub market_ref: Option<String>,
227    pub outcome: Option<String>,
228    pub token_id: Option<String>,
229    /// Unix seconds (inclusive)
230    pub start_ts: Option<i64>,
231    /// Unix seconds (inclusive)
232    pub end_ts: Option<i64>,
233    /// Max number of trades to return (exchange-specific caps may apply).
234    pub limit: Option<usize>,
235    /// Opaque pagination cursor from a previous response.
236    pub cursor: Option<String>,
237}
238
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
240#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
241pub struct OrderbookHistoryRequest {
242    pub market_id: String,
243    pub token_id: Option<String>,
244    pub start_ts: Option<i64>,
245    pub end_ts: Option<i64>,
246    pub limit: Option<usize>,
247    pub cursor: Option<String>,
248}