egui_charts/model/trading.rs
1//! Orders, positions and the account a broker reports.
2//!
3//! Prices are in the symbol's price units and quantities in its trading units. Every price and
4//! quantity is finite. Quantities are positive, except a position's, which is signed, and an
5//! order's filled quantity, which starts at 0.
6
7use chrono::{DateTime, Utc};
8
9/// A relative bound at `scale` for floating-point comparisons.
10/// Partial closes accumulate rounding, so quantities use a `1e-9` relative bound at the original
11/// quantity's scale, or the larger of the held and filled quantities without that history.
12/// Price-distance ties use four ulps for a single step.
13pub(crate) fn rounding_tolerance(scale: f64, relative: f64) -> f64 {
14 relative * scale.abs()
15}
16
17/// Identifies an order. The broker chooses it.
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct OrderId(
20 /// The broker's own identifier for the order.
21 pub String,
22);
23
24impl OrderId {
25 /// Create a new order ID from any string-like value.
26 pub fn new(id: impl Into<String>) -> Self {
27 Self(id.into())
28 }
29}
30
31impl std::fmt::Display for OrderId {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, "{}", self.0)
34 }
35}
36
37/// Buy or sell, shared by orders, executions, aggressor trades and strategy positions.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub enum OrderSide {
40 /// Buy side.
41 Buy,
42 /// Sell side.
43 Sell,
44}
45
46impl OrderSide {
47 /// Get display text
48 pub fn label(&self) -> &'static str {
49 match self {
50 OrderSide::Buy => "BUY",
51 OrderSide::Sell => "SELL",
52 }
53 }
54
55 /// Whether an exit on this side reduces `position`, a signed net quantity: not once the
56 /// position is flat or on this side, as after a close or a reverse.
57 pub(crate) fn reduces(self, position: f64) -> bool {
58 position != 0.0 && (self == OrderSide::Buy) != (position > 0.0)
59 }
60}
61
62/// How an order executes. Prices are in the symbol's price units.
63#[derive(Clone, Copy, Debug, PartialEq)]
64#[non_exhaustive]
65pub enum OrderType {
66 /// At the best price available now.
67 Market,
68 /// At this price or better.
69 Limit(f64),
70 /// As a market order once the market trades at this price.
71 Stop(f64),
72 /// As a limit order at `limit` once the market trades at `stop`.
73 StopLimit {
74 /// Trigger price for the stop order.
75 stop: f64,
76 /// Limit price of the order after triggering.
77 limit: f64,
78 },
79}
80
81impl OrderType {
82 /// Where an order of this type on `side` fills while the price moves from `start` to `end`,
83 /// or `None` if it does not: a market order at `start`, a limit or stop already reached at
84 /// `start` there, one the movement reaches at its price. A stop-limit order returns the
85 /// point where its stop triggered; the caller continues it from there as a limit order.
86 pub(crate) fn fill_price(&self, side: OrderSide, start: f64, end: f64) -> Option<f64> {
87 let buy = side == OrderSide::Buy;
88 let reach = |price: f64, at_or_below: bool| {
89 let reached = |at: f64| {
90 if at_or_below {
91 at <= price
92 } else {
93 at >= price
94 }
95 };
96 if reached(start) {
97 Some(start)
98 } else if reached(end) {
99 Some(price)
100 } else {
101 None
102 }
103 };
104 match *self {
105 OrderType::Market => Some(start),
106 OrderType::Limit(limit) => reach(limit, buy),
107 OrderType::Stop(stop) | OrderType::StopLimit { stop, .. } => reach(stop, !buy),
108 }
109 }
110}
111
112/// The role of an exit order attached to the net position in its symbol.
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub enum PositionBracket {
115 /// A profit-taking limit exit.
116 TakeProfit,
117 /// A protective stop exit.
118 StopLoss,
119}
120
121/// A take profit that closes its own part of an order: one of
122/// [`OrderRequest::take_profit_levels`].
123#[derive(Clone, Copy, Debug, PartialEq)]
124#[non_exhaustive]
125pub struct TakeProfitLevel {
126 /// The price at which it closes [`quantity`](Self::quantity): above the entry for a buy,
127 /// below for a sell.
128 pub price: f64,
129 /// Positive, in the symbol's trading units.
130 pub quantity: f64,
131}
132
133impl TakeProfitLevel {
134 /// A take profit that closes `quantity` at `price`.
135 pub fn new(price: f64, quantity: f64) -> Self {
136 Self { price, quantity }
137 }
138}
139
140/// An order as the user or the host asks for it.
141#[derive(Clone, Debug, PartialEq)]
142#[non_exhaustive]
143pub struct OrderRequest {
144 /// The symbol as the feed knows it.
145 pub symbol: String,
146 /// Buy or sell.
147 pub side: OrderSide,
148 /// Positive, in the symbol's trading units.
149 pub quantity: f64,
150 /// How the order executes, with its prices.
151 pub order_type: OrderType,
152 /// The price at which a take-profit order closes the filled quantity, for an account that
153 /// accepts [brackets](Capabilities::brackets): above the entry for a buy, below for a sell.
154 /// `None` while [`take_profit_levels`](Self::take_profit_levels) are set.
155 pub take_profit: Option<f64>,
156 /// Several take profits, each closing its own quantity, for an account that accepts
157 /// [take-profit levels](Capabilities::take_profit_levels), in the order given. Their
158 /// quantities add up to [`quantity`](Self::quantity). Empty for one take profit or none,
159 /// which [`take_profit`](Self::take_profit) holds.
160 pub take_profit_levels: Vec<TakeProfitLevel>,
161 /// The price at which a stop-loss order closes the filled quantity, for an account that
162 /// accepts [brackets](Capabilities::brackets): below the entry for a buy, above for a sell.
163 pub stop_loss: Option<f64>,
164 /// How long the order stays working.
165 pub time_in_force: TimeInForce,
166 /// Explicit link to the net position in this symbol. Ordinary opposite-side orders are
167 /// not brackets. Brokers should preserve this role on updates.
168 pub position_bracket: Option<PositionBracket>,
169}
170
171impl OrderRequest {
172 /// An order for `quantity` of `symbol`, without brackets, working for the day.
173 pub fn new(
174 symbol: impl Into<String>,
175 side: OrderSide,
176 quantity: f64,
177 order_type: OrderType,
178 ) -> Self {
179 Self {
180 symbol: symbol.into(),
181 side,
182 quantity,
183 order_type,
184 take_profit: None,
185 take_profit_levels: Vec::new(),
186 stop_loss: None,
187 time_in_force: TimeInForce::Day,
188 position_bracket: None,
189 }
190 }
191}
192
193/// How long an order stays working.
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
195#[non_exhaustive]
196pub enum TimeInForce {
197 /// Until the end of the trading day.
198 #[default]
199 Day,
200 /// Until the end of the trading week.
201 Week,
202 /// Until the end of the trading month.
203 Month,
204 /// Until it fills or is cancelled.
205 GoodTillCancelled,
206 /// Fills what it can at once and cancels the rest.
207 ImmediateOrCancel,
208 /// Fills in full at once, or not at all.
209 FillOrKill,
210}
211
212impl TimeInForce {
213 /// The short name a ticket shows, such as "GTC".
214 pub fn label(&self) -> &'static str {
215 match self {
216 Self::Day => "DAY",
217 Self::Week => "WEEK",
218 Self::Month => "MONTH",
219 Self::GoodTillCancelled => "GTC",
220 Self::ImmediateOrCancel => "IOC",
221 Self::FillOrKill => "FOK",
222 }
223 }
224}
225
226/// An order the broker holds. Pushed again on every change; the terminal replaces the order with
227/// the same id.
228#[derive(Clone, Debug, PartialEq)]
229#[non_exhaustive]
230pub struct Order {
231 /// The broker's id for the order: an event with the same id replaces it.
232 pub id: OrderId,
233 /// What was asked for.
234 pub request: OrderRequest,
235 /// Where the order stands.
236 pub status: OrderStatus,
237 /// The quantity filled so far, from 0 up to the requested quantity.
238 pub filled: f64,
239 /// The average price of the filled quantity, when the broker reports it.
240 pub fill_price: Option<f64>,
241 /// When the order was placed, when the broker reports it.
242 pub placed: Option<DateTime<Utc>>,
243 /// When the order filled in full, was cancelled or was rejected, when the broker reports it.
244 pub closed: Option<DateTime<Utc>>,
245}
246
247impl Order {
248 /// An order with nothing filled yet.
249 pub fn new(id: OrderId, request: OrderRequest, status: OrderStatus) -> Self {
250 Self {
251 id,
252 request,
253 status,
254 filled: 0.0,
255 fill_price: None,
256 placed: None,
257 closed: None,
258 }
259 }
260}
261
262/// Where an order stands. `Filled`, `Cancelled` and `Rejected` end the order.
263#[derive(Clone, Debug, PartialEq)]
264#[non_exhaustive]
265pub enum OrderStatus {
266 /// Accepted and waiting to fill, possibly partly filled.
267 Working,
268 /// Filled in full.
269 Filled,
270 /// Cancelled, possibly after a partial fill; `filled` says how much.
271 Cancelled,
272 /// Refused by the broker after it accepted the order and gave it an id, with its reason. An
273 /// order refused before that is answered with an error from
274 /// [`Broker::place`](crate::services::Broker::place) and never pushed.
275 Rejected(String),
276}
277
278/// The net position in one symbol.
279#[derive(Clone, Debug, PartialEq)]
280#[non_exhaustive]
281pub struct Position {
282 /// The symbol as the feed knows it.
283 pub symbol: String,
284 /// In the symbol's trading units, positive when long, negative when short. 0 means flat and
285 /// removes the position.
286 pub quantity: f64,
287 /// The average price the open quantity was entered at, in the symbol's price units.
288 pub average_price: f64,
289 /// The profit or loss at the current price in the account's currency, when the broker
290 /// reports it.
291 pub unrealized_pnl: Option<f64>,
292 /// The price the position is valued at, when the broker reports it.
293 pub last_price: Option<f64>,
294 /// The open quantity at its average price in the account's currency, when the broker reports
295 /// it.
296 pub trade_value: Option<f64>,
297 /// The open quantity at the last price in the account's currency, when the broker reports
298 /// it.
299 pub market_value: Option<f64>,
300 /// How many times the margin the position's market value is, such as 100 for 100:1, when
301 /// the broker reports it.
302 pub leverage: Option<f64>,
303 /// The margin the position holds in the account's currency, when the broker reports it.
304 pub margin: Option<f64>,
305 /// When the instrument expires, for futures and options.
306 pub expiration: Option<DateTime<Utc>>,
307}
308
309impl Position {
310 /// A position without a reported profit or loss.
311 pub fn new(symbol: impl Into<String>, quantity: f64, average_price: f64) -> Self {
312 Self {
313 symbol: symbol.into(),
314 quantity,
315 average_price,
316 unrealized_pnl: None,
317 last_price: None,
318 trade_value: None,
319 market_value: None,
320 leverage: None,
321 margin: None,
322 expiration: None,
323 }
324 }
325}
326
327/// One fill of an order.
328#[derive(Clone, Debug, PartialEq)]
329#[non_exhaustive]
330pub struct Execution {
331 /// The order this fill belongs to.
332 pub order: OrderId,
333 /// When the fill happened.
334 pub time: DateTime<Utc>,
335 /// The side of the order.
336 pub side: OrderSide,
337 /// The price of the fill, in the symbol's price units.
338 pub price: f64,
339 /// Positive, in the symbol's trading units.
340 pub quantity: f64,
341}
342
343impl Execution {
344 /// A fill of `quantity` at `price`.
345 pub fn new(
346 order: OrderId,
347 time: DateTime<Utc>,
348 side: OrderSide,
349 price: f64,
350 quantity: f64,
351 ) -> Self {
352 Self {
353 order,
354 time,
355 side,
356 price,
357 quantity,
358 }
359 }
360}
361
362/// The account behind one [`Broker`](crate::services::Broker) value.
363#[derive(Clone, Debug, PartialEq)]
364#[non_exhaustive]
365pub struct Account {
366 /// The currency balances are in, such as "USD".
367 pub currency: String,
368 /// The cash balance in `currency`.
369 pub balance: f64,
370 /// The balance plus the open positions' profit or loss, when the broker reports it.
371 pub equity: Option<f64>,
372 /// The profit or loss of the closed trades, net of commissions, when the broker reports it.
373 pub realized_pnl: Option<f64>,
374 /// The margin the open positions hold, when the broker reports it.
375 pub margin: Option<f64>,
376 /// The margin the working orders would hold once filled, when the broker reports it.
377 pub orders_margin: Option<f64>,
378 /// The equity less the margin of the positions and the working orders, when the broker
379 /// reports it.
380 pub available_funds: Option<f64>,
381 /// The order types the account accepts.
382 pub capabilities: Capabilities,
383}
384
385impl Account {
386 /// An account without a reported equity.
387 pub fn new(currency: impl Into<String>, balance: f64, capabilities: Capabilities) -> Self {
388 Self {
389 currency: currency.into(),
390 balance,
391 equity: None,
392 realized_pnl: None,
393 margin: None,
394 orders_margin: None,
395 available_funds: None,
396 capabilities,
397 }
398 }
399}
400
401/// A change of the account's cash balance, such as a closed trade's profit or a commission.
402#[derive(Clone, Debug, PartialEq)]
403#[non_exhaustive]
404pub struct BalanceChange {
405 /// When the balance changed.
406 pub time: DateTime<Utc>,
407 /// The balance after the change, in the account's currency.
408 pub balance: f64,
409 /// The change, positive for a credit and negative for a debit.
410 pub amount: f64,
411 /// What changed it, such as "Commission for order 12".
412 pub action: String,
413}
414
415impl BalanceChange {
416 /// A change of `amount` that left the balance at `balance`.
417 pub fn new(time: DateTime<Utc>, balance: f64, amount: f64, action: impl Into<String>) -> Self {
418 Self {
419 time,
420 balance,
421 amount,
422 action: action.into(),
423 }
424 }
425}
426
427/// One line of the account's trading journal: an order placed, changed, cancelled or filled, a
428/// position closed, or anything else the broker logs.
429#[derive(Clone, Debug, PartialEq)]
430#[non_exhaustive]
431pub struct JournalEntry {
432 /// When it happened.
433 pub time: DateTime<Utc>,
434 /// What happened, as a sentence.
435 pub text: String,
436}
437
438impl JournalEntry {
439 /// An entry saying `text`.
440 pub fn new(time: DateTime<Utc>, text: impl Into<String>) -> Self {
441 Self {
442 time,
443 text: text.into(),
444 }
445 }
446}
447
448/// The order types the account accepts. The terminal hides the controls for the others. By
449/// default the account accepts none; set the fields it accepts:
450///
451/// ```
452/// use egui_charts::model::Capabilities;
453///
454/// let mut capabilities = Capabilities::default();
455/// capabilities.market = true;
456/// capabilities.limit = true;
457/// ```
458#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
459#[non_exhaustive]
460pub struct Capabilities {
461 /// Accepts market orders.
462 pub market: bool,
463 /// Accepts limit orders.
464 pub limit: bool,
465 /// Accepts stop orders.
466 pub stop: bool,
467 /// Accepts stop-limit orders.
468 pub stop_limit: bool,
469 /// Accepts a take-profit and a stop-loss with an order, and with [`modify`](Self::modify)
470 /// adds them to a working order.
471 pub brackets: bool,
472 /// Accepts several take profits with an order, each closing its own quantity, through
473 /// [`OrderRequest::take_profit_levels`]. Needs [`brackets`](Self::brackets); without it the
474 /// order ticket offers one take profit.
475 pub take_profit_levels: bool,
476 /// Closes a position with an opposite market order, so the Close and Flatten controls work.
477 pub close: bool,
478 /// Reverses a position with an opposite market order of twice its size, so the Reverse
479 /// controls work.
480 pub reverse: bool,
481 /// Answers [`Broker::preview`](crate::services::Broker::preview), so the ticket offers to
482 /// preview an order before it is placed.
483 pub preview: bool,
484 /// Changes working orders through [`Broker::modify`](crate::services::Broker::modify), so
485 /// their lines on the chart can be dragged, and with [`brackets`](Self::brackets) exits
486 /// added to them.
487 pub modify: bool,
488}
489
490impl Capabilities {
491 /// Whether the account accepts orders of this type.
492 pub fn accepts(&self, order_type: &OrderType) -> bool {
493 match order_type {
494 OrderType::Market => self.market,
495 OrderType::Limit(_) => self.limit,
496 OrderType::Stop(_) => self.stop,
497 OrderType::StopLimit { .. } => self.stop_limit,
498 }
499 }
500}
501
502/// What a broker estimates for an order it has not accepted, from
503/// [`Broker::preview`](crate::services::Broker::preview). Every estimate is optional.
504#[derive(Clone, Debug, Default, PartialEq)]
505#[non_exhaustive]
506pub struct OrderPreview {
507 /// The estimated cost of the order in the account's currency.
508 pub cost: Option<f64>,
509 /// The estimated commission in the account's currency.
510 pub commission: Option<f64>,
511 /// The margin the order would need in the account's currency.
512 pub margin: Option<f64>,
513 /// What the broker warns of, such as a position the order would reverse.
514 pub warnings: Vec<String>,
515}
516
517/// How a symbol trades at the broker. The order ticket sizes orders by these rules.
518#[derive(Clone, Debug, PartialEq)]
519#[non_exhaustive]
520pub struct TradingRules {
521 /// The symbol as the feed knows it.
522 pub symbol: String,
523 /// Positive: quantities are whole multiples of it.
524 pub quantity_step: f64,
525 /// The smallest quantity, positive.
526 pub min_quantity: f64,
527 /// The largest quantity, if the broker limits it.
528 pub max_quantity: Option<f64>,
529 /// The profit or loss in the account's currency of one unit moving by one price unit: 1 for
530 /// a share, the contract multiplier for a future.
531 pub point_value: f64,
532 /// The profit or loss in the account's currency of one unit moving by one pip. `None` when the
533 /// broker reports none: the ticket then takes the instrument's
534 /// [`pip_size`](crate::model::Instrument::pip_size) times
535 /// [`point_value`](Self::point_value), and shows no pip value for an instrument without pips.
536 pub pip_value: Option<f64>,
537 /// The times in force the broker accepts for the symbol, the default first. Never empty.
538 pub time_in_force: Vec<TimeInForce>,
539 /// The trade value per unit of margin, such as 20 for 20:1, if the broker trades the symbol
540 /// on margin. The order ticket estimates the margin an order needs from it.
541 pub leverage: Option<f64>,
542}
543
544impl TradingRules {
545 /// Whole units from 1 without a maximum, a point value of 1 and day orders: the rules the
546 /// ticket assumes when the broker reports none.
547 pub fn new(symbol: impl Into<String>) -> Self {
548 Self {
549 symbol: symbol.into(),
550 quantity_step: 1.0,
551 min_quantity: 1.0,
552 max_quantity: None,
553 point_value: 1.0,
554 pip_value: None,
555 time_in_force: vec![TimeInForce::Day],
556 leverage: None,
557 }
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn fill_price_follows_the_side_and_the_movement() {
567 use OrderSide::{Buy, Sell};
568 let stop_limit = OrderType::StopLimit {
569 stop: 100.0,
570 limit: 101.0,
571 };
572 // (order type, side, start, end, fill)
573 let cases = [
574 (OrderType::Market, Buy, 103.0, 97.0, Some(103.0)),
575 (OrderType::Market, Sell, 97.0, 103.0, Some(97.0)),
576 // Gap: already beyond the price at the start, so filled there.
577 (OrderType::Limit(100.0), Buy, 98.0, 95.0, Some(98.0)),
578 (OrderType::Limit(100.0), Sell, 102.0, 105.0, Some(102.0)),
579 (OrderType::Stop(100.0), Buy, 102.0, 105.0, Some(102.0)),
580 (OrderType::Stop(100.0), Sell, 98.0, 95.0, Some(98.0)),
581 (stop_limit, Buy, 102.0, 105.0, Some(102.0)),
582 // Through: the movement reaches the price, so filled at it.
583 (OrderType::Limit(100.0), Buy, 103.0, 97.0, Some(100.0)),
584 (OrderType::Limit(100.0), Buy, 103.0, 100.0, Some(100.0)),
585 (OrderType::Limit(100.0), Sell, 97.0, 103.0, Some(100.0)),
586 (OrderType::Stop(100.0), Buy, 97.0, 103.0, Some(100.0)),
587 (OrderType::Stop(100.0), Sell, 103.0, 97.0, Some(100.0)),
588 (stop_limit, Buy, 97.0, 103.0, Some(100.0)),
589 (stop_limit, Sell, 103.0, 97.0, Some(100.0)),
590 // Not reached, including a movement away from the price.
591 (OrderType::Limit(100.0), Buy, 103.0, 101.0, None),
592 (OrderType::Limit(100.0), Sell, 97.0, 99.0, None),
593 (OrderType::Limit(100.0), Sell, 99.0, 97.0, None),
594 (OrderType::Stop(100.0), Buy, 97.0, 99.0, None),
595 (OrderType::Stop(100.0), Sell, 103.0, 101.0, None),
596 (stop_limit, Buy, 97.0, 99.0, None),
597 (stop_limit, Sell, 103.0, 101.0, None),
598 // A single price, as the paper account passes it.
599 (OrderType::Limit(100.0), Buy, 100.0, 100.0, Some(100.0)),
600 (OrderType::Limit(100.0), Buy, 100.5, 100.5, None),
601 (OrderType::Stop(100.0), Sell, 100.0, 100.0, Some(100.0)),
602 (OrderType::Stop(100.0), Sell, 100.5, 100.5, None),
603 ];
604 for (order_type, side, start, end, fill) in cases {
605 assert_eq!(
606 order_type.fill_price(side, start, end),
607 fill,
608 "{side:?} {order_type:?} from {start} to {end}"
609 );
610 }
611 }
612}