Skip to main content

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}