Skip to main content

egui_charts/services/
broker.rs

1//! Placing orders with the host's broker.
2
3use chrono::{DateTime, Utc};
4
5use super::Events;
6use crate::data::{FeedError, FeedStatus, MaybeSend, Reply};
7use crate::model::{
8    Account, BalanceChange, Execution, JournalEntry, Order, OrderId, OrderPreview, OrderRequest,
9    Position, TradingRules,
10};
11
12/// One trading account at the host's broker, real or simulated. The library holds no balances or
13/// positions: it shows what the broker pushes and places the orders the user confirms. Paper
14/// trading is an implementation of this trait. One value is one account; several accounts are
15/// several values.
16///
17/// Methods are called on the UI thread and must return at once. Talk to the broker elsewhere and
18/// answer or push from any thread, runtime or browser task.
19///
20/// Every price and quantity pushed must be finite. Quantities are positive, except a position's,
21/// which is signed, and an order's filled quantity, which runs from 0 to the requested quantity;
22/// the terminal skips events that break this. Prices are in the symbol's price units and
23/// quantities in its trading units.
24///
25/// A paper account that trades one symbol and fills market orders at a fixed price:
26///
27/// ```
28/// use chrono::Utc;
29/// use egui_charts::data::{FeedError, Reply};
30/// use egui_charts::model::{
31///     Account, Capabilities, Execution, Order, OrderId, OrderRequest, OrderSide, OrderStatus, OrderType, Position,
32/// };
33/// use egui_charts::services::{AccountEvent, Broker, Events};
34/// use std::sync::Mutex;
35///
36/// const SYMBOL: &str = "AAPL";
37/// const PRICE: f64 = 100.0;
38///
39/// #[derive(Default)]
40/// struct Paper {
41///     events: Mutex<Option<Events<AccountEvent>>>,
42///     /// Orders placed so far, and the net quantity held.
43///     state: Mutex<(u64, f64)>,
44/// }
45///
46/// impl Broker for Paper {
47///     fn connect(&self, events: Events<AccountEvent>) {
48///         let mut capabilities = Capabilities::default();
49///         capabilities.market = true;
50///         events.push(AccountEvent::Account(Account::new("USD", 10_000.0, capabilities)));
51///         // Market orders fill at once, so no order is working; a position is pushed when one is
52///         // open.
53///         let held = self.state.lock().unwrap().1;
54///         if held != 0.0 {
55///             events.push(AccountEvent::Position(Position::new(SYMBOL, held, PRICE)));
56///         }
57///         *self.events.lock().unwrap() = Some(events);
58///     }
59///
60///     fn place(&self, order: OrderRequest, reply: Reply<OrderId>) {
61///         if order.order_type != OrderType::Market || order.symbol != SYMBOL {
62///             return reply.send(Err(FeedError::NotSupported));
63///         }
64///         let (id, held) = {
65///             let mut state = self.state.lock().unwrap();
66///             state.0 += 1;
67///             state.1 += match order.side {
68///                 OrderSide::Buy => order.quantity,
69///                 OrderSide::Sell => -order.quantity,
70///             };
71///             (OrderId(format!("paper-{}", state.0)), state.1)
72///         };
73///         reply.send(Ok(id.clone()));
74///
75///         let Some(events) = self.events.lock().unwrap().clone() else {
76///             return;
77///         };
78///         let execution = Execution::new(id.clone(), Utc::now(), order.side, PRICE, order.quantity);
79///         let position = Position::new(order.symbol.clone(), held, PRICE);
80///         let mut filled = Order::new(id, order, OrderStatus::Filled);
81///         filled.filled = filled.request.quantity;
82///         events.push(AccountEvent::Order(filled));
83///         events.push(AccountEvent::Execution(execution));
84///         events.push(AccountEvent::Position(position));
85///     }
86///
87///     fn cancel(&self, id: OrderId, reply: Reply<()>) {
88///         reply.send(Err(FeedError::Other(format!("order {id} is already filled"))));
89///     }
90/// }
91/// ```
92pub trait Broker: MaybeSend + 'static {
93    /// Pushes the account (balance, currency, capabilities) first, then the working orders and
94    /// open positions, then orders, positions, executions and connection status as they change.
95    /// A broker that keeps history pushes it after the open state: the finished orders, the
96    /// balance changes and the journal, oldest first, which the account panel lists.
97    /// The terminal calls it when it starts and again, with a new stream, after the stream lagged;
98    /// each stream starts from nothing, so push the whole current state again. The old stream is
99    /// inactive from then on: push into the newest one only.
100    ///
101    /// The terminal announces, with a notification and the execution sound for fills, a working
102    /// limit or stop order it has not seen on the stream, a rejected order and each execution,
103    /// when they are pushed after `connect` returned while the broker is live. What is pushed
104    /// before `connect` returns, or while the broker reports that it is not live, restates the
105    /// account and is not announced: push the current state then, before returning or between
106    /// [`FeedStatus::Connecting`] and [`FeedStatus::Live`].
107    ///
108    /// A stream starts as connected. While the connection to the broker is not live, push
109    /// [`AccountEvent::Status`] with [`FeedStatus::Connecting`] or [`FeedStatus::Disconnected`],
110    /// on each new stream too, and [`FeedStatus::Live`] once it is live again; until then the
111    /// account panel and the order ticket mark the broker and the terminal reports it as not
112    /// connected. A broker that never pushes a status counts as connected. Meanwhile the terminal
113    /// sends no order, cancellation or change: it reports the ones the user confirms as refused,
114    /// and a position order confirmed earlier that still waits for its trading rules too, and
115    /// sends none of them later. It asks for [`rules`](Self::rules) and
116    /// [`history`](Self::history) only while the broker is live, and once it is live again it asks
117    /// again for those that were on their way when it stopped being live, or that failed meanwhile
118    /// or with a [recoverable](FeedError::is_recoverable) error.
119    fn connect(&self, events: Events<AccountEvent>);
120
121    /// Sends `order` to the broker. The terminal calls this only after the user confirmed the
122    /// order, or the host performed the place order command itself, so do not ask again. Answer
123    /// with the broker's id once the order is accepted, or with a
124    /// [`FeedError`](crate::data::FeedError) the terminal shows. The order's
125    /// [`AccountEvent::Order`] may arrive before or after that answer.
126    fn place(&self, order: OrderRequest, reply: Reply<OrderId>);
127
128    /// Cancels the working order `id`. The terminal calls this only after the user confirmed, so
129    /// do not ask again. Answer once the broker accepted the cancellation, or with a
130    /// [`FeedError`](crate::data::FeedError) the terminal shows; the order's
131    /// [`OrderStatus::Cancelled`](crate::model::OrderStatus::Cancelled) update may arrive before
132    /// or after that answer.
133    fn cancel(&self, id: OrderId, reply: Reply<()>);
134
135    /// Replaces the working order `id` with `order`, which differs in its prices or brackets,
136    /// after the user dragged one of its lines, added or removed a take profit or stop loss, and
137    /// confirmed. Called only for an account whose [`Capabilities::modify`](crate::model::Capabilities::modify) is set. Answer
138    /// once the broker accepted the change; push the changed order as an [`AccountEvent::Order`].
139    /// By default every change is refused as [`FeedError::NotSupported`].
140    fn modify(&self, id: OrderId, order: OrderRequest, reply: Reply<()>) {
141        let _ = (id, order);
142        reply.send(Err(FeedError::NotSupported));
143    }
144
145    /// Estimates what `order` would cost without placing it. Called only for an account whose
146    /// [`Capabilities::preview`](crate::model::Capabilities::preview) is set, when the user asks
147    /// to preview an order; the terminal places the previewed order only after the user confirms
148    /// it. By default every preview is refused as [`FeedError::NotSupported`].
149    fn preview(&self, order: OrderRequest, reply: Reply<OrderPreview>) {
150        let _ = order;
151        reply.send(Err(FeedError::NotSupported));
152    }
153
154    /// Answers how `symbol` trades: its quantity step, minimum and maximum quantity, point value
155    /// and times in force. By default the answer is [`FeedError::NotSupported`], and the order
156    /// ticket assumes [`TradingRules::new`].
157    fn rules(&self, symbol: &str, reply: Reply<TradingRules>) {
158        let _ = symbol;
159        reply.send(Err(FeedError::NotSupported));
160    }
161
162    /// Answers one page of finished history older than what [`connect`](Self::connect) pushed, for
163    /// the kind and range of `request`. By default the answer is [`FeedError::NotSupported`], and
164    /// only the pushed history exists.
165    ///
166    /// A page holds [`AccountEvent::Order`] with a finished status, [`AccountEvent::Execution`],
167    /// [`AccountEvent::Balance`] or [`AccountEvent::Journal`] events, as the request's
168    /// [`HistoryKind`] says, oldest first, and never working orders or positions: those come only
169    /// from the pushed stream. The first page of a request holds the newest entries of its range
170    /// and each page after it, asked for with the [`next`](HistoryPage::next) cursor, the entries
171    /// before the previous one. Entries that were pushed live may appear again in a page: an order
172    /// is matched by its id and other events by value, so repeat them unchanged. Answer for the
173    /// account `connect` last served; a consumer drops an answer that arrives after the account
174    /// changed.
175    ///
176    /// The account panel asks once the account arrived, while the broker is live, and the user
177    /// scrolls to the end of a history: first for the range that ends one second after the oldest
178    /// entry it lists (for an order, when it closed), so that entries sharing that time are not
179    /// skipped, then for each `next` until it is `None` or the history lists 1,000 entries. After
180    /// an error, which it shows unless it is `NotSupported`, it asks for that history again only
181    /// after the next `connect`; after a [recoverable](FeedError::is_recoverable) error or one that
182    /// arrives while the broker reports that it is not live, it asks again once the broker
183    /// reports that it is live again.
184    fn history(&self, request: HistoryRequest, reply: Reply<HistoryPage>) {
185        let _ = request;
186        reply.send(Err(FeedError::NotSupported));
187    }
188}
189
190/// Which finished history [`Broker::history`] is asked for.
191#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
192#[non_exhaustive]
193pub enum HistoryKind {
194    /// Finished orders, as [`AccountEvent::Order`].
195    Orders,
196    /// Fills, as [`AccountEvent::Execution`].
197    Executions,
198    /// Changes of the cash balance, as [`AccountEvent::Balance`].
199    Balance,
200    /// Lines of the trading journal, as [`AccountEvent::Journal`].
201    Journal,
202}
203
204/// A request for one page of older history.
205#[derive(Clone, Debug, PartialEq, Eq)]
206#[non_exhaustive]
207pub struct HistoryRequest {
208    /// The history asked for.
209    pub kind: HistoryKind,
210    /// Inclusive start of the range, in UTC; `None` reaches back as far as the broker keeps.
211    pub from: Option<DateTime<Utc>>,
212    /// Exclusive end of the range, in UTC; `None` ends at the newest entry.
213    pub to: Option<DateTime<Utc>>,
214    /// The [`next`](HistoryPage::next) of the previous page of this request; `None` for the first.
215    pub cursor: Option<String>,
216    /// The number of entries wanted, at least 1. The page may hold fewer, and more only if
217    /// entries share a timestamp.
218    pub limit: u32,
219}
220
221impl HistoryRequest {
222    /// The first page of `kind` over all the broker keeps, of at most `limit` entries.
223    pub fn new(kind: HistoryKind, limit: u32) -> Self {
224        Self {
225            kind,
226            from: None,
227            to: None,
228            cursor: None,
229            limit,
230        }
231    }
232}
233
234/// One page of older history.
235#[derive(Clone, Debug, Default, PartialEq)]
236#[non_exhaustive]
237pub struct HistoryPage {
238    /// The entries, oldest first.
239    pub events: Vec<AccountEvent>,
240    /// The cursor of the page before this one, or `None` when the range holds nothing older.
241    pub next: Option<String>,
242}
243
244impl HistoryPage {
245    /// A page of `events` with the cursor of the page before it.
246    pub fn new(events: Vec<AccountEvent>, next: Option<String>) -> Self {
247        Self { events, next }
248    }
249}
250
251/// A change to the account a [`Broker`] reports.
252#[derive(Clone, Debug, PartialEq)]
253#[non_exhaustive]
254pub enum AccountEvent {
255    /// The account's balance, currency or capabilities.
256    Account(Account),
257    /// Adds the order, or replaces the one with its id.
258    Order(Order),
259    /// Adds the position in its symbol, or replaces it; a quantity of 0 removes it.
260    Position(Position),
261    /// One fill, drawn on charts of its order's symbol: push the order first.
262    Execution(Execution),
263    /// The connection to the broker; a stream starts as [`FeedStatus::Live`]. See
264    /// [`Broker::connect`].
265    Status(FeedStatus),
266    /// A change of the cash balance, for the balance history.
267    Balance(BalanceChange),
268    /// A line of the trading journal.
269    Journal(JournalEntry),
270}