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