Skip to main content

polyoxide_venue/
trading.rs

1//! The trading trait, its records, and the stream it reports on.
2
3use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6use std::task::{Context, Poll};
7
8use futures_core::Stream;
9use rust_decimal::Decimal;
10
11use crate::{
12    ClassifiedError, ClientOrderId, Extensions, FillId, MarketKey, OrderId, Side, TradeId,
13    UnixMillis,
14};
15
16/// Places, cancels and reconciles orders on one venue, and reports what
17/// happens to them.
18///
19/// Every method returns [`ClassifiedError`], which an implementation builds
20/// from its module error with `?`. Held as `Arc<DynTrading<'static>>` by code
21/// that picks its venue at run time.
22///
23/// # Requests
24///
25/// An [`OrderRequest`] names an outcome or instrument key, a [`Side`], a price
26/// and a size in the instrument's native quantity. The venue's own options
27/// ride in its `extensions`, and the venue reads only the option types it
28/// declares: [`place`](Self::place) refuses a request carrying any other, as
29/// [`Class::InvalidRequest`](crate::Class::InvalidRequest), and sends nothing
30/// ([`Extensions::ensure_declared`]). So does a key of another venue or
31/// product, and so does a request whose `post_only`, `reduce_only` or time in
32/// force the venue cannot honour: it is refused, never ignored.
33///
34/// # Kills
35///
36/// An immediate-or-cancel order that matched nothing, or a fill-or-kill order
37/// that could not fill in full, is not an error: `place` returns `Ok` with
38/// [`OrderStatus::Killed`], and its reason. `filled` says how much an
39/// immediate-or-cancel order matched before its remainder was cancelled.
40///
41/// # Events
42///
43/// [`events`](Self::events) is fed from an unbounded queue, so a slow consumer
44/// loses no acknowledgement or fill. The venue replays nothing: what happened
45/// while the connection that carries fills was down is not sent again.
46///
47/// The stream's first item is [`TradingEvent::Resync`], unless the stream
48/// cannot open, when its only item is that error. `Resync` is yielded once the
49/// connection's first attempt to connect has ended, whether it went live or
50/// began in an outage, so a fill between `events()` returning and the first
51/// subscription is covered by a reconciliation. `Resync` comes again after
52/// every reconnect of the connection that carries fills, and when the venue
53/// voids a fill it reported. The consumer reconciles its inventory each
54/// time through [`open_orders`](Self::open_orders) and the venue's trade history.
55#[dynosaur::dynosaur(pub DynTrading = dyn(box) Trading)]
56pub trait Trading: Send + Sync {
57    /// Places `order` and returns it as the venue accepted it: open, filled,
58    /// or killed.
59    fn place(
60        &self,
61        order: OrderRequest,
62    ) -> impl Future<Output = Result<Order, ClassifiedError>> + Send;
63
64    /// Cancels the order `id`.
65    ///
66    /// Cancelling an order that has already ended, or that the venue does not
67    /// know, is an error classed
68    /// [`Class::VenueRefusal`](crate::Class::VenueRefusal). The order's final
69    /// state arrives through [`events`](Self::events) and
70    /// [`open_orders`](Self::open_orders).
71    fn cancel(&self, id: &OrderId) -> impl Future<Output = Result<(), ClassifiedError>> + Send;
72
73    /// Cancels every open order, or every open order on `key`, and returns the
74    /// ids the venue cancelled. An order the venue did not cancel is left out
75    /// of them and stays open, so [`open_orders`](Self::open_orders) shows it.
76    fn cancel_all(
77        &self,
78        key: Option<&MarketKey>,
79    ) -> impl Future<Output = Result<Vec<OrderId>, ClassifiedError>> + Send;
80
81    /// Every open order, or every open order on `key`.
82    fn open_orders(
83        &self,
84        key: Option<&MarketKey>,
85    ) -> impl Future<Output = Result<Vec<Order>, ClassifiedError>> + Send;
86
87    /// Every position the account holds.
88    fn positions(&self) -> impl Future<Output = Result<Vec<Position>, ClassifiedError>> + Send;
89
90    /// Every balance the account holds.
91    fn balances(&self) -> impl Future<Output = Result<Vec<Balance>, ClassifiedError>> + Send;
92
93    /// The account's acknowledgements and fills from now on, beginning with a
94    /// [`TradingEvent::Resync`] and carrying one after every reconnect.
95    ///
96    /// Each call returns a stream of its own, which sees every event from its
97    /// first subscription on; its leading `Resync` covers what came earlier
98    /// (D3). An order may be reported (acked or filled) before the
99    /// [`place`](Self::place) that created it returns. See [`TradingEvents`]
100    /// for when the stream ends.
101    fn events(&self) -> TradingEvents;
102}
103
104/// The stream [`Trading::events`] returns: the account's acknowledgements and
105/// fills, from an unbounded queue.
106///
107/// It is `Send`, `Unpin` and `'static`, so a consumer can move it onto a task
108/// of its own, whether it reached the venue statically or through
109/// `Arc<DynTrading<'static>>`. (A trait method returning `impl Stream + 'static`
110/// would lose the `'static` through the dyn wrapper, so the stream is this
111/// named type.)
112///
113/// Its first item is [`TradingEvent::Resync`], unless the stream cannot open,
114/// when its only item is that error. The venue replays nothing: after
115/// every reconnect of the connection that carries fills, and when the venue
116/// voids a fill it reported (a trade that failed after it matched, which the
117/// stream has no event to take back), the stream yields `Resync` before any
118/// later fill. What happened while the connection was down will not arrive, and
119/// the consumer reconciles through [`Trading::open_orders`] and the venue's
120/// trade history.
121///
122/// An `Err` item the venue recovered from, such as a lost connection it
123/// re-established, is followed by more items. After one it could not recover
124/// from, the stream ends. `None` means the feed has stopped for good.
125pub struct TradingEvents(
126    Pin<Box<dyn Stream<Item = Result<TradingEvent, ClassifiedError>> + Send + 'static>>,
127);
128
129impl TradingEvents {
130    /// Wraps the stream a [`Trading`] implementation feeds from its queue.
131    pub fn new(
132        stream: impl Stream<Item = Result<TradingEvent, ClassifiedError>> + Send + 'static,
133    ) -> Self {
134        Self(Box::pin(stream))
135    }
136}
137
138impl fmt::Debug for TradingEvents {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        f.write_str("TradingEvents(..)")
141    }
142}
143
144impl Stream for TradingEvents {
145    type Item = Result<TradingEvent, ClassifiedError>;
146
147    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
148        self.0.as_mut().poll_next(cx)
149    }
150
151    fn size_hint(&self) -> (usize, Option<usize>) {
152        self.0.size_hint()
153    }
154}
155
156/// How long an order rests.
157///
158/// An order that expires at a set time is a venue option.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
160#[non_exhaustive]
161pub enum TimeInForce {
162    /// Rests until it fills or is cancelled.
163    #[default]
164    GoodTilCancelled,
165    /// Matches what it can at once; the remainder is cancelled.
166    ImmediateOrCancel,
167    /// Fills in full at once, or not at all. A venue that cancels one partway,
168    /// such as for self-trade prevention, ends it
169    /// [`Cancelled`](OrderStatus::Cancelled), possibly partly filled, not
170    /// [`Killed`](OrderStatus::Killed).
171    FillOrKill,
172}
173
174/// An order to place.
175#[derive(Debug, Clone, PartialEq)]
176#[non_exhaustive]
177pub struct OrderRequest {
178    /// The instrument, or the outcome of an event, to trade.
179    pub key: MarketKey,
180    /// Buy or sell.
181    pub side: Side,
182    /// The limit price.
183    pub price: Decimal,
184    /// The size, in the instrument's native quantity.
185    pub size: Decimal,
186    /// How long it rests.
187    pub time_in_force: TimeInForce,
188    /// The caller's id for it. The venue echoes it, or the implementation
189    /// keeps it for the orders it placed, as its documentation says; a venue
190    /// that can do neither refuses a request that carries one.
191    pub client_order_id: Option<ClientOrderId>,
192    /// Rest only: refuse to take liquidity.
193    pub post_only: bool,
194    /// Only reduce a position, never open or grow one.
195    pub reduce_only: bool,
196    /// The venue's own options; one it does not declare is refused.
197    pub extensions: Extensions,
198}
199
200impl OrderRequest {
201    /// A good-til-cancelled order to `side` `size` of `key` at `price`, with
202    /// no client id, neither flag and no extensions.
203    pub fn new(key: MarketKey, side: Side, price: Decimal, size: Decimal) -> Self {
204        Self {
205            key,
206            side,
207            price,
208            size,
209            time_in_force: TimeInForce::default(),
210            client_order_id: None,
211            post_only: false,
212            reduce_only: false,
213            extensions: Extensions::new(),
214        }
215    }
216}
217
218/// Why an order was killed.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220#[non_exhaustive]
221pub enum KillReason {
222    /// An immediate-or-cancel order's remainder, cancelled because nothing
223    /// more matched at once. `filled` says how much did.
224    Unmatched,
225    /// A fill-or-kill order that could not fill in full.
226    Unfillable,
227}
228
229/// Where an order stands.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
231#[non_exhaustive]
232pub enum OrderStatus {
233    /// Resting, possibly partly filled.
234    Open,
235    /// Filled in full.
236    Filled,
237    /// Cancelled, by the caller or the venue, possibly partly filled.
238    Cancelled,
239    /// Killed by its time in force: a defined outcome, not an error.
240    Killed {
241        /// Why.
242        reason: KillReason,
243    },
244}
245
246/// An order as the venue reports it.
247#[derive(Debug, Clone, PartialEq)]
248#[non_exhaustive]
249pub struct Order {
250    /// The venue's id for it.
251    pub id: OrderId,
252    /// The instrument or outcome it trades.
253    pub key: MarketKey,
254    /// Buy or sell.
255    pub side: Side,
256    /// The limit price.
257    pub price: Decimal,
258    /// The size ordered, in the instrument's native quantity.
259    pub size: Decimal,
260    /// How much has filled.
261    pub filled: Decimal,
262    /// Where it stands.
263    pub status: OrderStatus,
264    /// How long it rests.
265    pub time_in_force: Option<TimeInForce>,
266    /// The caller's id for it.
267    pub client_order_id: Option<ClientOrderId>,
268    /// When the venue accepted it.
269    pub created: Option<UnixMillis>,
270    /// The venue's own facts.
271    pub extensions: Extensions,
272}
273
274impl Order {
275    /// The order `id` to `side` `size` of `key` at `price`, with nothing
276    /// filled yet.
277    pub fn new(
278        id: OrderId,
279        key: MarketKey,
280        side: Side,
281        price: Decimal,
282        size: Decimal,
283        status: OrderStatus,
284    ) -> Self {
285        Self {
286            id,
287            key,
288            side,
289            price,
290            size,
291            filled: Decimal::ZERO,
292            status,
293            time_in_force: None,
294            client_order_id: None,
295            created: None,
296            extensions: Extensions::new(),
297        }
298    }
299}
300
301/// Whether a fill took or made liquidity.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
303#[non_exhaustive]
304pub enum Liquidity {
305    /// The order rested and was matched.
306    Maker,
307    /// The order matched a resting one.
308    Taker,
309}
310
311/// One order's part in a trade.
312#[derive(Debug, Clone, PartialEq)]
313#[non_exhaustive]
314pub struct Fill {
315    /// The venue's id for the fill.
316    pub id: FillId,
317    /// The order filled.
318    pub order_id: OrderId,
319    /// The trade it was part of.
320    pub trade_id: Option<TradeId>,
321    /// The instrument or outcome traded.
322    pub key: MarketKey,
323    /// The order's side.
324    pub side: Side,
325    /// The price.
326    pub price: Decimal,
327    /// The size, in the instrument's native quantity.
328    pub size: Decimal,
329    /// The quote-currency amount, where the venue reports one.
330    pub notional: Option<Decimal>,
331    /// The fee charged, in the quote currency unless the venue's own facts say
332    /// otherwise.
333    pub fee: Option<Decimal>,
334    /// Whether the order made or took liquidity.
335    pub liquidity: Option<Liquidity>,
336    /// The caller's id for the order.
337    pub client_order_id: Option<ClientOrderId>,
338    /// When it filled.
339    pub time: UnixMillis,
340    /// The venue's own facts.
341    pub extensions: Extensions,
342}
343
344impl Fill {
345    /// The fill `id` of order `order_id`: `size` of `key` at `price`, at
346    /// `time`.
347    pub fn new(
348        id: FillId,
349        order_id: OrderId,
350        key: MarketKey,
351        side: Side,
352        price: Decimal,
353        size: Decimal,
354        time: UnixMillis,
355    ) -> Self {
356        Self {
357            id,
358            order_id,
359            trade_id: None,
360            key,
361            side,
362            price,
363            size,
364            notional: None,
365            fee: None,
366            liquidity: None,
367            client_order_id: None,
368            time,
369            extensions: Extensions::new(),
370        }
371    }
372}
373
374/// What the account holds of one instrument or outcome.
375#[derive(Debug, Clone, PartialEq)]
376#[non_exhaustive]
377pub struct Position {
378    /// The instrument or outcome.
379    pub key: MarketKey,
380    /// The size held, in the instrument's native quantity: positive long,
381    /// negative short. A position in an event's outcome is never negative,
382    /// since each outcome has its own key.
383    pub size: Decimal,
384    /// The average price paid.
385    pub average_price: Option<Decimal>,
386    /// The venue's own facts.
387    pub extensions: Extensions,
388}
389
390impl Position {
391    /// `size` of `key`.
392    pub fn new(key: MarketKey, size: Decimal) -> Self {
393        Self {
394            key,
395            size,
396            average_price: None,
397            extensions: Extensions::new(),
398        }
399    }
400}
401
402/// What the account holds of one asset.
403#[derive(Debug, Clone, PartialEq)]
404#[non_exhaustive]
405pub struct Balance {
406    /// The asset, as the venue names it.
407    pub asset: String,
408    /// The whole balance.
409    pub total: Option<Decimal>,
410    /// What is free to trade.
411    pub available: Option<Decimal>,
412    /// The venue's own facts.
413    pub extensions: Extensions,
414}
415
416impl Balance {
417    /// The balance of `asset`, with no amounts yet.
418    pub fn new(asset: impl Into<String>) -> Self {
419        Self {
420            asset: asset.into(),
421            total: None,
422            available: None,
423            extensions: Extensions::new(),
424        }
425    }
426}
427
428/// What [`Trading::events`] reports.
429#[derive(Debug, Clone, PartialEq)]
430#[non_exhaustive]
431pub enum TradingEvent {
432    /// The venue accepted, changed or ended an order.
433    Ack(Order),
434    /// An order filled, in part or in full.
435    Fill(Fill),
436    /// Reconcile through [`Trading::open_orders`] and the venue's trade
437    /// history: the stream cannot vouch for what it reported or missed before
438    /// this point.
439    ///
440    /// It is the stream's first item, unless the stream cannot open, when its
441    /// only item is that error; it comes again after every reconnect of the
442    /// connection that carries fills, and comes when the venue voids a fill it
443    /// already reported.
444    Resync,
445}