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}