rustrade_core/exchange.rs
1//! Trait contracts for exchange integrations.
2//!
3//! Concrete exchange clients (KuCoin, Binance, …) implement [`ExchangeClient`]
4//! so the bot framework can stay exchange-agnostic. A client crate like
5//! `exchange-apiws` already provides most of this — these traits are the
6//! framework-side view.
7
8use std::time::Duration;
9
10use async_trait::async_trait;
11
12use crate::error::Result;
13use crate::market::{MarketDataEvent, Symbol};
14use crate::types::{Candle, Fill, Order, Position};
15
16/// Optional adapter capabilities, queried via [`ExchangeClient::supports`].
17///
18/// The framework consults this to degrade gracefully when an adapter
19/// doesn't implement a feature an [`Order`] or strategy requests — e.g.
20/// rejecting an order with `Order.stop = Some(...)` against an adapter
21/// that returns `false` for [`Capability::StopOrders`] rather than
22/// silently dropping the attachment.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24#[non_exhaustive]
25pub enum Capability {
26 /// Adapter accepts `Order.stop` and translates to native stop orders.
27 StopOrders,
28 /// Adapter rejects post-only orders that would cross the book as taker.
29 PostOnly,
30 /// Adapter honours `Order.reduce_only`.
31 ReduceOnly,
32 /// Adapter supports `OrderKind::Ioc`.
33 Ioc,
34 /// Adapter supports `OrderKind::Fok`.
35 Fok,
36 /// Adapter can stream a public market-data feed alongside trading.
37 /// Most do; spot-only HTTP adapters may not.
38 PublicFeed,
39 /// Adapter pushes fill / order-update events on a private feed.
40 PrivateFeed,
41}
42
43/// Status of an order as reported by the exchange.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum OrderStatus {
46 /// Submitted, not yet on the book.
47 Pending,
48 /// Resting on the book or in the matching engine.
49 Open,
50 /// Partially filled; still resting for the remainder.
51 PartiallyFilled,
52 /// Fully filled.
53 Filled,
54 /// Cancelled before full fill.
55 Cancelled,
56 /// Rejected by the exchange.
57 Rejected,
58}
59
60/// What the bot framework needs from an exchange to trade.
61///
62/// This trait is intentionally narrow — the full surface of a real exchange
63/// client (ws token management, stop orders, funding history, account tiers)
64/// belongs in the concrete adapter crate, not here. The framework only
65/// needs to: place orders, close positions, read balance and position state.
66///
67/// # Async + object-safe
68///
69/// `async_trait` is used so `Arc<dyn ExchangeClient>` works — downstream
70/// code can swap concrete exchanges at runtime without generics propagating
71/// through the whole system.
72///
73/// # Example
74///
75/// A stub adapter useful for examples and tests. Real adapters connect
76/// to a network and report actual state.
77///
78/// ```
79/// use async_trait::async_trait;
80/// use rustrade_core::{Capability, ExchangeClient, Order, Position, Result, Symbol};
81///
82/// struct StubExchange;
83///
84/// #[async_trait]
85/// impl ExchangeClient for StubExchange {
86/// fn name(&self) -> &str { "stub" }
87/// async fn place_order(&self, _order: &Order) -> Result<String> {
88/// Ok("order-1".into())
89/// }
90/// async fn cancel_all(&self, _symbol: &Symbol) -> Result<usize> { Ok(0) }
91/// async fn close_position(&self, _symbol: &Symbol, _p: &Position) -> Result<String> {
92/// Ok("close-1".into())
93/// }
94/// async fn get_position(&self, _symbol: &Symbol) -> Result<Position> {
95/// Ok(Position::FLAT)
96/// }
97/// async fn get_balance(&self, _currency: &str) -> Result<f64> { Ok(0.0) }
98/// fn supports(&self, c: Capability) -> bool {
99/// matches!(c, Capability::ReduceOnly)
100/// }
101/// }
102/// ```
103#[async_trait]
104pub trait ExchangeClient: Send + Sync + 'static {
105 /// Short, lowercase exchange identifier — e.g. `"kucoin"`.
106 fn name(&self) -> &str;
107
108 /// Place an order. Returns the exchange-assigned order id.
109 async fn place_order(&self, order: &Order) -> Result<String>;
110
111 /// Cancel all open orders for a symbol. Returns the count cancelled.
112 async fn cancel_all(&self, symbol: &Symbol) -> Result<usize>;
113
114 /// Close the given position with a market order. Returns the exchange
115 /// order id of the close.
116 async fn close_position(&self, symbol: &Symbol, position: &Position) -> Result<String>;
117
118 /// Fetch the current position for a symbol (or `Position::FLAT` if flat).
119 async fn get_position(&self, symbol: &Symbol) -> Result<Position>;
120
121 /// Fetch the current balance in the given currency.
122 async fn get_balance(&self, currency: &str) -> Result<f64>;
123
124 /// Does this adapter support the given optional capability?
125 ///
126 /// The default returns `false` for every variant — adapters opt in by
127 /// overriding. This is intentionally conservative: a new adapter that
128 /// forgets to override won't quietly accept orders it can't execute.
129 fn supports(&self, _capability: Capability) -> bool {
130 false
131 }
132
133 /// Base-asset units per one contract for the given symbol.
134 ///
135 /// For spot exchanges this is `1.0` for every symbol — one unit traded
136 /// equals one unit of the base asset. For futures, it's the contract
137 /// multiplier (e.g. `0.001` for KuCoin XBTUSDTM where one contract is
138 /// 0.001 BTC). The risk layer's
139 /// [`PositionSizer`](https://docs.rs/rustrade-risk/latest/rustrade_risk/sizing/struct.PositionSizer.html)
140 /// uses this to convert margin × leverage into a contract count.
141 ///
142 /// The default returns `1.0` — appropriate for spot adapters. Futures
143 /// adapters override.
144 fn contract_value(&self, _symbol: &Symbol) -> f64 {
145 1.0
146 }
147}
148
149/// A source of live market data (WebSocket feed, backtest replay, simulator).
150///
151/// Implementors push events into the bot via the
152/// [`MarketDataBus`](crate::bus::MarketDataBus) that the supervisor creates.
153/// `MarketSource` is intended to be wrapped by a `TradingService` in
154/// `rustrade-supervisor` so it inherits lifecycle management and
155/// auto-restart; this trait just documents the contract on the data side.
156///
157/// # Example
158///
159/// A loopback source that publishes a single tick and exits. Production
160/// sources hold the `MarketDataBus` sender they were constructed with
161/// and publish to it from `run`.
162///
163/// ```
164/// use async_trait::async_trait;
165/// use rustrade_core::{MarketSource, Result};
166///
167/// struct OneShotSource {
168/// name: String,
169/// }
170///
171/// #[async_trait]
172/// impl MarketSource for OneShotSource {
173/// fn name(&self) -> &str { &self.name }
174/// async fn run(&self) -> Result<()> {
175/// // In a real impl: connect to feed, loop publishing events to
176/// // the bus, return Ok(()) on clean shutdown.
177/// Ok(())
178/// }
179/// fn is_live(&self) -> bool { false }
180/// }
181/// ```
182///
183/// # Cancellation contract
184///
185/// `run` does **not** take a `CancellationToken` directly. Cancellation is
186/// expected to flow through the wrapping `TradingService` — when the
187/// supervisor cancels that service's token, it drops the
188/// `MarketSource::run` future at its next `.await`.
189///
190/// Implementors must therefore be **drop-safe**: any open resources
191/// (WebSocket connections, HTTP sessions, file handles) must release
192/// cleanly when their containing future is dropped. In practice this
193/// means:
194///
195/// - Use `tokio::select!` against external events only inside your own
196/// loop, not against an externally-owned cancel signal here.
197/// - Don't hold a `MutexGuard` across an `.await` that could be dropped
198/// mid-flight — dropping a guard is fine, but holding one while the
199/// future is destructured can deadlock the lock.
200/// - If you need explicit teardown, perform it in a `Drop` impl on the
201/// implementing type rather than at the end of `run`.
202#[async_trait]
203pub trait MarketSource: Send + Sync + 'static {
204 /// Short identifier for logging — typically the exchange name.
205 fn name(&self) -> &str;
206
207 /// Begin streaming events. Should run until the feed terminates or
208 /// the caller drops the returned future (see the cancellation contract
209 /// in the trait docs).
210 async fn run(&self) -> Result<()>;
211
212 /// Is the feed currently receiving data?
213 fn is_live(&self) -> bool;
214}
215
216/// Received fill events from the exchange's private feed.
217///
218/// Adapters implement this to route fills into the bot. Most exchanges push
219/// both order updates and fill events; this trait abstracts the "fill" part.
220///
221/// # Example
222///
223/// An in-memory fill source backed by a [`tokio::sync::mpsc`] channel —
224/// useful for tests and replay drivers.
225///
226/// ```
227/// use async_trait::async_trait;
228/// use rustrade_core::{Fill, FillSource};
229/// use tokio::sync::mpsc;
230/// use tokio::sync::Mutex;
231///
232/// struct ChannelFills {
233/// rx: Mutex<mpsc::UnboundedReceiver<Fill>>,
234/// }
235///
236/// #[async_trait]
237/// impl FillSource for ChannelFills {
238/// async fn next_fill(&self) -> Option<Fill> {
239/// self.rx.lock().await.recv().await
240/// }
241/// }
242/// ```
243#[async_trait]
244pub trait FillSource: Send + Sync + 'static {
245 /// Await the next fill. Returns `None` when the stream ends.
246 async fn next_fill(&self) -> Option<Fill>;
247}
248
249/// Received order-book / market-data events from the exchange's public feed.
250///
251/// # Example
252///
253/// A simple channel-backed event source — typical for tests that push
254/// scripted ticks/candles into the bot.
255///
256/// ```
257/// use async_trait::async_trait;
258/// use rustrade_core::{EventSource, MarketDataEvent};
259/// use tokio::sync::mpsc;
260/// use tokio::sync::Mutex;
261///
262/// struct ChannelEvents {
263/// rx: Mutex<mpsc::UnboundedReceiver<MarketDataEvent>>,
264/// }
265///
266/// #[async_trait]
267/// impl EventSource for ChannelEvents {
268/// async fn next_event(&self) -> Option<MarketDataEvent> {
269/// self.rx.lock().await.recv().await
270/// }
271/// }
272/// ```
273#[async_trait]
274pub trait EventSource: Send + Sync + 'static {
275 /// Await the next event. Returns `None` when the stream ends.
276 async fn next_event(&self) -> Option<MarketDataEvent>;
277}
278
279/// Periodic candle source — separate from [`MarketSource`] because
280/// candle polling has a fundamentally different shape (pull, paced)
281/// than streaming events (push, unbounded).
282///
283/// Spot-only adapters don't need to implement this; the framework will
284/// only spawn a candle poller when one is wired via
285/// `Bot::with_candle_poller`. Futures adapters with native candle
286/// endpoints (KuCoin, Binance, Bybit, …) implement it directly.
287///
288/// # Example
289///
290/// A fixed-series source useful for backtests and replays. The
291/// framework's poller will dedupe by `Candle::time`, so repeated polls
292/// returning the same head are safe.
293///
294/// ```
295/// use std::time::Duration;
296/// use async_trait::async_trait;
297/// use rustrade_core::{Candle, CandleSource, Result, Symbol};
298///
299/// struct FixedCandles {
300/// candles: Vec<Candle>,
301/// }
302///
303/// #[async_trait]
304/// impl CandleSource for FixedCandles {
305/// fn name(&self) -> &str { "fixed" }
306/// async fn poll(
307/// &self,
308/// _symbol: &Symbol,
309/// _interval: Duration,
310/// limit: usize,
311/// ) -> Result<Vec<Candle>> {
312/// Ok(self.candles.iter().rev().take(limit).rev().copied().collect())
313/// }
314/// }
315/// ```
316#[async_trait]
317pub trait CandleSource: Send + Sync + 'static {
318 /// Short identifier for logging — typically the exchange name.
319 fn name(&self) -> &str;
320
321 /// Fetch up to `limit` of the most recent completed candles for
322 /// `symbol` at the given interval. Implementors return them in
323 /// chronological order (oldest first). If the exchange's native
324 /// endpoint returns newest-first, sort before returning.
325 async fn poll(&self, symbol: &Symbol, interval: Duration, limit: usize) -> Result<Vec<Candle>>;
326}