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}