Skip to main content

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}