Skip to main content

egui_charts/ui/trading/account_panel/
mod.rs

1//! The account panel: the connected account's summary, positions, working orders, order and
2//! balance history and trading journal, shown at the bottom of the terminal.
3
4mod history;
5mod view;
6
7use std::collections::{HashMap, HashSet};
8
9use chrono::{DateTime, Utc};
10use egui::Color32;
11
12use history::Paging;
13
14use super::Trading;
15use crate::model::{
16    BalanceChange, Execution, JournalEntry, Order, OrderId, OrderRequest, OrderStatus, OrderType,
17    Position,
18};
19use crate::services::{AccountEvent, HistoryKind};
20
21const TAB_HEIGHT: f32 = 38.0;
22const HEADER_HEIGHT: f32 = 52.0;
23const ROW_HEIGHT: f32 = 48.0;
24/// Room at the end of each row for its edit and close buttons and the column chooser.
25const ACTIONS_WIDTH: f32 = 64.0;
26/// Maximum entries retained in each completed account history.
27pub(super) const HISTORY_LIMIT: usize = 1_000;
28
29/// The account's orders and executions, shared by the ladder, chart and account history.
30#[derive(Default)]
31pub(super) struct Fills {
32    pub(super) orders: HashMap<OrderId, OrderRequest>,
33    pub(super) executions: Vec<Execution>,
34    /// How many executions at the front came from pages that the stream has not repeated.
35    paged: usize,
36    working: HashSet<OrderId>,
37    filled: HashMap<OrderId, f64>,
38    /// Latest fill for each symbol, independent of execution-history eviction.
39    latest: HashMap<String, Execution>,
40}
41
42impl Fills {
43    pub(super) fn order(&mut self, order: &Order) {
44        self.orders.insert(order.id.clone(), order.request.clone());
45        if order.status == OrderStatus::Working {
46            self.working.insert(order.id.clone());
47        } else {
48            self.working.remove(&order.id);
49        }
50        if let Some(fill) = self
51            .executions
52            .iter()
53            .filter(|fill| fill.order == order.id)
54            .max_by_key(|fill| fill.time)
55            .cloned()
56        {
57            self.remember(&order.request.symbol, &fill);
58        }
59    }
60
61    pub(super) fn execution(&mut self, fill: &Execution) {
62        let (repeated, dropped) = push_live(&mut self.executions, &mut self.paged, fill);
63        // A fill that a page brought first was counted then.
64        if !repeated {
65            *self.filled.entry(fill.order.clone()).or_default() += fill.quantity;
66        }
67        if let Some(order) = self.orders.get(&fill.order) {
68            let symbol = order.symbol.clone();
69            self.remember(&symbol, fill);
70        }
71        if let Some(old) = dropped {
72            self.forget(&old.order);
73        }
74    }
75
76    pub(super) fn forget(&mut self, id: &OrderId) {
77        if !self.working.contains(id) && !self.executions.iter().any(|fill| &fill.order == id) {
78            self.orders.remove(id);
79            self.filled.remove(id);
80        }
81    }
82
83    fn remember(&mut self, symbol: &str, fill: &Execution) {
84        if self
85            .latest
86            .get(symbol)
87            .is_none_or(|old| old.time <= fill.time)
88        {
89            self.latest.insert(symbol.to_owned(), fill.clone());
90        }
91    }
92
93    pub(super) fn price(&self, symbol: &str) -> Option<f64> {
94        self.latest.get(symbol).map(|fill| fill.price)
95    }
96
97    pub(super) fn filled_quantity(&self, id: &OrderId) -> f64 {
98        self.filled.get(id).copied().unwrap_or_default()
99    }
100}
101
102/// Appends `entry` from the stream to `list`, whose first `paged` entries came from pages, in
103/// place of the equal page entry when a page brought it first, then drops the oldest entry
104/// beyond [`HISTORY_LIMIT`]. Returns whether `entry` repeated a page entry, and the entry
105/// dropped.
106fn push_live<T: PartialEq + Clone>(
107    list: &mut Vec<T>,
108    paged: &mut usize,
109    entry: &T,
110) -> (bool, Option<T>) {
111    let repeated = list[..*paged].iter().position(|listed| listed == entry);
112    if let Some(at) = repeated {
113        list.remove(at);
114        *paged -= 1;
115    }
116    list.push(entry.clone());
117    let dropped = if list.len() > HISTORY_LIMIT {
118        *paged = paged.saturating_sub(1);
119        Some(list.remove(0))
120    } else {
121        None
122    };
123    (repeated.is_some(), dropped)
124}
125
126/// What the user asked for in the [`AccountPanel`] that the host carries out. Cancelling or
127/// changing an order and closing a position go to the broker from the panel itself.
128#[derive(Clone, Debug, PartialEq)]
129#[non_exhaustive]
130pub enum AccountPanelAction {
131    /// Show this symbol on the chart.
132    SelectSymbol(String),
133    /// Connect to the account at this index of [`AccountPanel::accounts`].
134    SelectAccount(usize),
135    /// Disconnect from the broker.
136    Disconnect,
137    /// Open the account's settings, such as resetting a [paper
138    /// account](crate::services::PaperAccount).
139    Settings,
140    /// Collapse the panel to its tab bar, or restore it.
141    Minimize,
142    /// Give the panel the full height, or restore it.
143    Maximize,
144    /// The table in view as comma-separated values, for the host to save as `name`.csv.
145    Export {
146        /// The table's name, such as "Positions".
147        name: String,
148        /// A header line, then one line per row.
149        csv: String,
150    },
151}
152
153/// The account panel of [`Trading`], shown with [`Trading::show_account_panel`].
154///
155/// A tab named after the broker sits above the account selectors and the account's summary:
156/// balance, equity, realised and unrealised profit and loss, margin, available funds and the
157/// margin of working orders, each shown when the broker reports it. The tab marks a broker that
158/// reports that it is connecting or disconnected, until it reports that it is live. Below are the
159/// positions, working orders, finished orders, balance changes and trading journal the broker
160/// pushes, each with a column chooser and an export. Order History also lists individual
161/// executions. A position offers close, and an order offers edit and cancel on hover; each waits
162/// for the user's confirmation like the chart's lines. Clicking
163/// a symbol asks the host to show it. Each history keeps the most recent 1,000 entries; active
164/// orders are retained independently. Scrolling to the end of a history asks
165/// [`Broker::history`](crate::services::Broker::history) for the entries before its oldest
166/// one, a page at a time, until the broker has nothing older or the history lists 1,000
167/// entries. Each entry is listed once, whether a page or the broker's stream brings it first,
168/// and pages change no working order or position.
169#[derive(Default)]
170pub struct AccountPanel {
171    /// The broker's name, shown on the tab and in the account selector.
172    pub broker: String,
173    /// The accounts the host offers at the broker, such as "Paper 1". The selector shows the
174    /// [`active`](Self::active) one with the account's currency and switches between them.
175    pub accounts: Vec<String>,
176    /// The index in [`accounts`](Self::accounts) of the connected account.
177    pub active: usize,
178    tab: Tab,
179    hidden: HashSet<(Tab, &'static str)>,
180    history: Vec<Order>,
181    pub(super) fills: Fills,
182    show_executions: bool,
183    balance: Vec<BalanceChange>,
184    /// How many balance changes at the front came from pages that the stream has not repeated.
185    balance_paged: usize,
186    journal: Vec<JournalEntry>,
187    /// The same for `journal`.
188    journal_paged: usize,
189    /// Paging through the broker's older history, by kind, since the stream started.
190    paging: HashMap<HistoryKind, Paging>,
191    /// The order whose edit dialog is open, and its changed request.
192    pub(super) editing: Option<(Order, OrderRequest)>,
193}
194
195#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
196enum Tab {
197    #[default]
198    Positions,
199    Orders,
200    History,
201    Balance,
202    Journal,
203    Executions,
204}
205
206impl Tab {
207    const ALL: [Tab; 5] = [
208        Tab::Positions,
209        Tab::Orders,
210        Tab::History,
211        Tab::Balance,
212        Tab::Journal,
213    ];
214
215    fn title(self) -> &'static str {
216        match self {
217            Tab::Positions => "Positions",
218            Tab::Orders => "Orders",
219            Tab::History => "Order History",
220            Tab::Balance => "Balance History",
221            Tab::Journal => "Trading Journal",
222            Tab::Executions => "Executions",
223        }
224    }
225
226    /// The broker history this tab lists and pages through.
227    fn history(self) -> Option<HistoryKind> {
228        match self {
229            Tab::History => Some(HistoryKind::Orders),
230            Tab::Executions => Some(HistoryKind::Executions),
231            Tab::Balance => Some(HistoryKind::Balance),
232            Tab::Journal => Some(HistoryKind::Journal),
233            Tab::Positions | Tab::Orders => None,
234        }
235    }
236
237    fn columns(self) -> &'static [Column] {
238        const fn left(title: &'static str, width: f32) -> Column {
239            Column {
240                title,
241                right: false,
242                width,
243            }
244        }
245        const fn right(title: &'static str, width: f32) -> Column {
246            Column {
247                title,
248                right: true,
249                width,
250            }
251        }
252        match self {
253            Tab::Positions => {
254                const COLUMNS: &[Column] = &[
255                    left("Symbol", 134.0),
256                    left("Side", 44.0),
257                    right("Qty", 44.0),
258                    right("Avg Fill Price", 84.0),
259                    right("Last Price", 76.0),
260                    right("Unrealized P&L", 96.0),
261                    right("Unrealized P&L %", 96.0),
262                    right("Trade Value", 96.0),
263                    right("Market Value", 96.0),
264                    right("Leverage", 56.0),
265                    right("Margin", 80.0),
266                    left("Expiration Date", 96.0),
267                ];
268                COLUMNS
269            }
270            Tab::Orders => {
271                const COLUMNS: &[Column] = &[
272                    left("Symbol", 134.0),
273                    left("Side", 44.0),
274                    left("Type", 60.0),
275                    right("Qty", 44.0),
276                    right("Limit Price", 80.0),
277                    right("Stop Price", 80.0),
278                    right("Take Profit", 80.0),
279                    right("Stop Loss", 80.0),
280                    left("Status", 76.0),
281                    left("Placing Time", 130.0),
282                    left("Order ID", 60.0),
283                ];
284                COLUMNS
285            }
286            Tab::History => {
287                const COLUMNS: &[Column] = &[
288                    left("Symbol", 134.0),
289                    left("Side", 44.0),
290                    left("Type", 60.0),
291                    right("Qty", 44.0),
292                    right("Limit Price", 80.0),
293                    right("Stop Price", 80.0),
294                    right("Fill Price", 80.0),
295                    left("Status", 76.0),
296                    left("Placing Time", 130.0),
297                    left("Closing Time", 130.0),
298                    left("Order ID", 60.0),
299                ];
300                COLUMNS
301            }
302            Tab::Balance => {
303                const COLUMNS: &[Column] = &[
304                    left("Time", 130.0),
305                    right("Balance Before", 110.0),
306                    right("Balance After", 110.0),
307                    right("Realized P&L", 110.0),
308                    left("Action", 300.0),
309                ];
310                COLUMNS
311            }
312            Tab::Executions => {
313                const COLUMNS: &[Column] = &[
314                    left("Symbol", 134.0),
315                    left("Side", 44.0),
316                    right("Qty", 44.0),
317                    right("Fill Price", 80.0),
318                    left("Time", 130.0),
319                    left("Order ID", 60.0),
320                ];
321                COLUMNS
322            }
323            Tab::Journal => {
324                const COLUMNS: &[Column] = &[left("Time", 130.0), left("Text", 500.0)];
325                COLUMNS
326            }
327        }
328    }
329}
330
331/// A table column: its title, whether its cells are right-aligned, and its smallest width.
332struct Column {
333    title: &'static str,
334    right: bool,
335    width: f32,
336}
337
338/// One table cell: its text and colour, a unit in small muted text after it, and whether it is a
339/// symbol chip.
340struct Cell {
341    text: String,
342    color: Color32,
343    unit: String,
344    symbol: bool,
345    /// The logo of a symbol chip's instrument, when the host supplied one.
346    logo: Option<String>,
347}
348
349/// A table row: its cells, the symbol a click on its chip shows, and what its edit and close
350/// buttons do.
351struct Row {
352    cells: Vec<Cell>,
353    symbol: Option<String>,
354    edit: Option<RowAction>,
355    close: Option<RowAction>,
356}
357
358enum RowAction {
359    Position(Position),
360    Order(Order),
361}
362
363/// The rising colour for a gain, the falling colour for a loss, and the text colour when `text`
364/// shows zero.
365fn pnl_color(theme: &crate::theme::Theme, value: f64, text: &str) -> Color32 {
366    if !text.bytes().any(|b| matches!(b, b'1'..=b'9')) {
367        theme.ui.text
368    } else if value > 0.0 {
369        theme.chart.candle_up
370    } else {
371        theme.chart.candle_down
372    }
373}
374
375fn time(time: Option<DateTime<Utc>>) -> String {
376    time.map_or_else(String::new, |time| {
377        time.format("%Y-%m-%d %H:%M:%S").to_string()
378    })
379}
380
381fn kind(order_type: &OrderType) -> &'static str {
382    match order_type {
383        OrderType::Market => "Market",
384        OrderType::Limit(_) => "Limit",
385        OrderType::Stop(_) => "Stop",
386        OrderType::StopLimit { .. } => "Stop Limit",
387    }
388}
389
390fn status(ctx: &egui::Context, status: &OrderStatus) -> String {
391    match status {
392        OrderStatus::Working => "Working".to_owned(),
393        OrderStatus::Filled => "Filled".to_owned(),
394        OrderStatus::Cancelled => "Cancelled".to_owned(),
395        OrderStatus::Rejected(reason) => crate::localization::format_text(
396            ctx,
397            "Rejected: {reason}",
398            &[("reason", reason.as_str())],
399        ),
400    }
401}
402
403impl AccountPanel {
404    /// Keeps the history the broker pushes: finished orders, balance changes and the journal.
405    pub(super) fn record(&mut self, event: &AccountEvent) {
406        if let AccountEvent::Order(order) = event
407            && self
408                .editing
409                .as_ref()
410                .is_some_and(|(old, _)| old.id == order.id && old != order)
411        {
412            self.editing = None;
413        }
414        if let AccountEvent::Account(account) = event
415            && !account.capabilities.modify
416        {
417            self.editing = None;
418        }
419        if let AccountEvent::Order(order) = event {
420            self.fills.order(order);
421        }
422        match event {
423            AccountEvent::Order(order) if order.status != OrderStatus::Working => {
424                self.history.retain(|finished| finished.id != order.id);
425                self.history.push(order.clone());
426                if self.history.len() > HISTORY_LIMIT {
427                    let old = self.history.remove(0);
428                    self.fills.forget(&old.id);
429                }
430            }
431            AccountEvent::Execution(fill) => self.fills.execution(fill),
432            AccountEvent::Balance(change) => {
433                push_live(&mut self.balance, &mut self.balance_paged, change);
434            }
435            AccountEvent::Journal(entry) => {
436                push_live(&mut self.journal, &mut self.journal_paged, entry);
437            }
438            _ => {}
439        }
440    }
441
442    /// Forgets the history, as a new stream from the broker starts from nothing.
443    pub(super) fn clear(&mut self) {
444        self.history.clear();
445        self.fills = Fills::default();
446        self.balance.clear();
447        self.balance_paged = 0;
448        self.journal.clear();
449        self.journal_paged = 0;
450        self.paging.clear();
451        self.editing = None;
452    }
453}
454
455/// Decimals that show `quantity` without rounding, up to 8.
456pub(super) fn quantity_decimals(quantity: f64) -> usize {
457    (0..8)
458        .find(|&decimals| {
459            let scaled = quantity * 10f64.powi(decimals as i32);
460            (scaled - scaled.round()).abs() < 1e-6
461        })
462        .unwrap_or(8)
463}
464
465/// The rows in view as comma-separated values, with the shown columns' titles as the header.
466/// Cells holding a comma or a quote are quoted.
467fn csv(columns: &[(usize, &Column)], rows: &[Row]) -> String {
468    let field = |text: &str| {
469        if text.contains([',', '"', '\n', '\r']) {
470            format!("\"{}\"", text.replace('"', "\"\""))
471        } else {
472            text.to_owned()
473        }
474    };
475    let mut out = columns
476        .iter()
477        .map(|(_, column)| field(column.title))
478        .collect::<Vec<_>>()
479        .join(",");
480    for row in rows {
481        out.push('\n');
482        out += &columns
483            .iter()
484            .map(|&(index, _)| {
485                let cell = &row.cells[index];
486                let text = if cell.unit.is_empty() {
487                    cell.text.clone()
488                } else {
489                    format!("{} {}", cell.text, cell.unit)
490                };
491                field(&text)
492            })
493            .collect::<Vec<_>>()
494            .join(",");
495    }
496    out
497}
498
499#[cfg(test)]
500mod tests {
501    use std::sync::Arc;
502
503    use egui_kittest::{Harness, kittest::Queryable};
504
505    use super::*;
506    use crate::model::OrderSide;
507    use crate::ui::trading::tests::Recorder;
508    use crate::ui_kit::notifications::Toasts;
509
510    /// A position's close button can be found by scanning the rows, without the pointer on one.
511    #[test]
512    fn row_actions_show_at_rest() {
513        let broker = Arc::new(Recorder {
514            state: vec![AccountEvent::Position(Position::new("SYM", 1.0, 100.0))],
515            ..Default::default()
516        });
517        let mut trading = Trading::default();
518        trading.set_service(broker);
519        trading.poll(&egui::Context::default(), &mut Toasts::new());
520        trading.account_panel.broker = "Broker".into();
521        let mut harness = Harness::builder()
522            .with_size(egui::vec2(1000.0, 400.0))
523            .with_context_setup(egui_extras::install_image_loaders)
524            .build_ui_state(
525                |ui, trading: &mut Trading| {
526                    trading.show_account_panel(ui, "SYM");
527                },
528                trading,
529            );
530        harness.run();
531        harness.get_by_label("Close position");
532    }
533
534    /// In panels as narrow as the demo gives on 320 and 393 point screens, the Trade, minimize
535    /// and maximize buttons stay apart and inside the panel, with or without the broker's
536    /// connection mark.
537    #[test]
538    fn tab_strip_buttons_stay_apart_in_narrow_panels() {
539        use crate::data::FeedStatus;
540        for width in [264.0, 337.0] {
541            for status in [
542                None,
543                Some(FeedStatus::Connecting),
544                Some(FeedStatus::Disconnected),
545            ] {
546                let mut trading = Trading::default();
547                trading.set_service(Arc::new(Recorder::default()));
548                trading.poll(&egui::Context::default(), &mut Toasts::new());
549                if let Some(status) = status {
550                    trading.apply(
551                        &egui::Context::default(),
552                        AccountEvent::Status(status),
553                        &mut Toasts::new(),
554                    );
555                }
556                trading.account_panel.broker = "Paper Trading".into();
557                // The harness keeps an 8 point margin around the panel.
558                let mut harness = Harness::builder()
559                    .with_size(egui::vec2(width + 16.0, 320.0))
560                    .with_context_setup(egui_extras::install_image_loaders)
561                    .build_ui_state(
562                        |ui, trading: &mut Trading| {
563                            trading.show_account_panel(ui, "SYM");
564                        },
565                        trading,
566                    );
567                harness.run();
568                let rects = ["Trade", "Minimize panel", "Maximize panel"]
569                    .map(|label| harness.get_by_label(label).rect());
570                for (index, rect) in rects.iter().enumerate() {
571                    let case = format!("{status:?} at {width}: {index}");
572                    assert!(rect.min.x >= 8.0 && rect.max.x <= 8.0 + width, "{case}");
573                    for other in &rects[index + 1..] {
574                        assert!(!rect.intersect(*other).is_positive(), "{case}");
575                    }
576                }
577            }
578        }
579    }
580
581    #[test]
582    fn history_keeps_each_partial_fill_and_discards_stale_edits() {
583        let mut panel = AccountPanel::default();
584        let order = Order::new(
585            crate::model::OrderId::new("7"),
586            OrderRequest::new("SYM", OrderSide::Buy, 2.0, OrderType::Limit(100.0)),
587            OrderStatus::Working,
588        );
589        let fill = Execution::new(order.id.clone(), Utc::now(), OrderSide::Buy, 99.0, 1.0);
590        panel.record(&AccountEvent::Execution(fill.clone()));
591        panel.record(&AccountEvent::Execution(fill.clone()));
592        assert_eq!(panel.fills.executions, vec![fill.clone(), fill]);
593        for event in [
594            AccountEvent::Order({
595                let mut partial = order.clone();
596                partial.filled = 1.0;
597                partial
598            }),
599            AccountEvent::Account(crate::model::Account::new(
600                "USD",
601                1000.0,
602                Default::default(),
603            )),
604        ] {
605            panel.editing = Some((order.clone(), order.request.clone()));
606            panel.record(&event);
607            assert!(panel.editing.is_none());
608        }
609        panel.clear();
610        assert!(panel.fills.executions.is_empty());
611    }
612
613    #[test]
614    fn history_retention_preserves_working_orders_and_latest_fills() {
615        let mut panel = AccountPanel::default();
616        let mut order = Order::new(
617            crate::model::OrderId::new("active"),
618            OrderRequest::new("OLD", OrderSide::Buy, 5_000.0, OrderType::Market),
619            OrderStatus::Working,
620        );
621        let time = DateTime::from_timestamp(1_704_067_200, 0).unwrap();
622        panel.record(&AccountEvent::Order(order.clone()));
623        panel.record(&AccountEvent::Execution(Execution::new(
624            order.id.clone(),
625            time,
626            OrderSide::Buy,
627            99.0,
628            1.0,
629        )));
630        order.status = OrderStatus::Filled;
631        for i in 0..2_005 {
632            order.id = crate::model::OrderId::new(i.to_string());
633            order.request.symbol = format!("NEW-{i}");
634            let time = time + chrono::Duration::seconds(i);
635            panel.record(&AccountEvent::Order(order.clone()));
636            panel.record(&AccountEvent::Execution(Execution::new(
637                order.id.clone(),
638                time,
639                OrderSide::Buy,
640                100.0,
641                1.0,
642            )));
643            panel.record(&AccountEvent::Balance(BalanceChange::new(
644                time,
645                1000.0,
646                1.0,
647                i.to_string(),
648            )));
649            panel.record(&AccountEvent::Journal(JournalEntry::new(
650                time,
651                i.to_string(),
652            )));
653        }
654        assert_eq!(panel.history.len(), 1_000);
655        assert_eq!(panel.balance.len(), 1_000);
656        assert_eq!(panel.journal.len(), 1_000);
657        assert_eq!(panel.fills.executions.len(), 1_000);
658        assert_eq!(panel.history[0].id.0, "1005");
659        assert_eq!(panel.fills.executions[0].order.0, "1005");
660        assert_eq!(panel.fills.orders.len(), 1_001);
661        assert!(
662            panel
663                .fills
664                .orders
665                .contains_key(&crate::model::OrderId::new("active"))
666        );
667        assert_eq!(panel.fills.price("OLD"), Some(99.0));
668        assert_eq!(panel.fills.price("NEW-0"), Some(100.0));
669        assert_eq!(panel.fills.price("NEW-2004"), Some(100.0));
670        // Orders without fills must not accumulate metadata either.
671        order.status = OrderStatus::Cancelled;
672        for i in 2_005..4_010 {
673            order.id = crate::model::OrderId::new(i.to_string());
674            panel.record(&AccountEvent::Order(order.clone()));
675        }
676        assert!(panel.fills.orders.len() <= 2_001);
677        let active = crate::model::OrderId::new("active");
678        for _ in 0..1_005 {
679            panel.record(&AccountEvent::Execution(Execution::new(
680                active.clone(),
681                time,
682                OrderSide::Buy,
683                99.0,
684                1.0,
685            )));
686        }
687        assert_eq!(panel.fills.executions.len(), 1_000);
688        assert_eq!(panel.fills.filled_quantity(&active), 1_006.0);
689        panel.clear();
690        assert!(panel.fills.orders.is_empty());
691        assert_eq!(panel.fills.price("OLD"), None);
692    }
693
694    #[test]
695    fn csv_round_trips_quoted_journal_text_and_visible_columns() {
696        let columns: Vec<_> = Tab::Journal.columns().iter().enumerate().collect();
697        let rows = vec![Row {
698            cells: ["time", "line\rbreak"]
699                .into_iter()
700                .map(|text| Cell {
701                    text: text.into(),
702                    color: Color32::WHITE,
703                    unit: String::new(),
704                    symbol: false,
705                    logo: None,
706                })
707                .collect(),
708            symbol: None,
709            edit: None,
710            close: None,
711        }];
712        for shown in [&columns[..], &columns[1..]] {
713            let exported = csv(shown, &rows);
714            let mut reader = ::csv::Reader::from_reader(exported.as_bytes());
715            assert_eq!(reader.headers().unwrap().len(), shown.len());
716            assert_eq!(
717                reader.records().next().unwrap().unwrap().iter().next_back(),
718                Some("line\rbreak")
719            );
720        }
721    }
722
723    #[test]
724    fn last_fill_belongs_to_its_order_symbol_and_newest_execution() {
725        let mut fills = Fills::default();
726        let order = Order::new(
727            OrderId::new("a"),
728            OrderRequest::new("ES", OrderSide::Buy, 2.0, OrderType::Market),
729            crate::model::OrderStatus::Working,
730        );
731        fills.order(&order);
732        let now = chrono::Utc::now();
733        let mut fill = Execution::new(order.id.clone(), now, OrderSide::Buy, 100.0, 1.0);
734        fills.execution(&fill);
735        fill.time -= chrono::Duration::seconds(1);
736        fill.price = 99.0;
737        fills.execution(&fill);
738        fill.order = OrderId::new("unknown");
739        fills.execution(&fill);
740        assert_eq!(fills.price("ES"), Some(100.0));
741        assert_eq!(fills.price("NQ"), None);
742        let mut delayed = order.clone();
743        delayed.id = fill.order.clone();
744        delayed.request.symbol = "NQ".into();
745        fills.order(&delayed);
746        assert_eq!(fills.price("NQ"), Some(99.0));
747        assert_eq!(fills.price("ES"), Some(100.0));
748    }
749}