Skip to main content

egui_charts/services/
paper.rs

1//! A simulated account behind the [`Broker`] service.
2
3use std::collections::HashMap;
4use std::sync::Mutex;
5
6use chrono::{DateTime, Utc};
7
8use super::{AccountEvent, Broker, Events};
9use crate::data::{FeedError, Reply};
10use crate::model::rounding_tolerance;
11use crate::model::{
12    Account, BalanceChange, Capabilities, Execution, JournalEntry, Order, OrderId, OrderRequest,
13    OrderSide, OrderStatus, OrderType, Position, PositionBracket, TimeInForce, TradingRules,
14};
15
16/// A paper account: a [`Broker`] that fills orders against the prices the host feeds it, so the
17/// order ticket, the chart's order and position lines and the account panel work as they do with
18/// a real account.
19///
20/// Feed it the last price of every symbol it trades with [`set_price`](Self::set_price), live or
21/// replayed. During replay, call [`set_replay_time`](Self::set_replay_time) before each price so
22/// fills and history use the source bar's time. Only good-till-cancelled orders are supported;
23/// the account has no trading-session calendar. Market orders fill at the last price; limit and
24/// stop orders fill at the first price that reaches them, a limit at that price or better;
25/// a triggered stop-limit works on as a limit order. An order's take profits and stop loss become
26/// exit orders once it fills, for the quantity the fill opened: take-profit levels each close their
27/// own quantity, the first ones first when the fill opened less than the order's quantity, as after
28/// closing an opposite position. When a take profit fills, the stop loss shrinks by its quantity
29/// and is cancelled once nothing is left; when the stop loss fills, the take profits are cancelled.
30/// Each symbol has one net position with its average
31/// price: a fill against it books the realised profit or loss of the closed quantity, and a fill
32/// beyond it opens the other side at the fill price. A position holds its market value divided by
33/// the leverage as margin, and a working order the same for its quantity at its price. Each fill
34/// pays the commission rate times its value. Placement and modification reserve buying power
35/// across all symbols, including working orders and their commissions. Market orders that
36/// reduce exposure remain possible when funds are insufficient. Position exits only reduce the
37/// remaining position.
38/// Every change is pushed as an [`AccountEvent`], with
39/// the most recent 1,000 finished orders, executions, balance changes and journal entries.
40/// The history bound keeps a reconnect snapshot below the event stream's burst limit for
41/// ordinary interactive accounts.
42///
43/// ```
44/// use std::sync::Arc;
45/// use egui_charts::services::PaperAccount;
46///
47/// let paper = Arc::new(PaperAccount::new("USD", 100_000.0).leverage(10.0).commission(0.001));
48/// paper.set_price("AAPL", 187.5);
49/// // trading.set_service(paper.clone());
50/// ```
51pub struct PaperAccount {
52    currency: String,
53    initial: f64,
54    leverage: f64,
55    commission: f64,
56    state: Mutex<State>,
57}
58
59#[derive(Default)]
60struct State {
61    events: Option<Events<AccountEvent>>,
62    replay_time: Option<DateTime<Utc>>,
63    next_id: u64,
64    balance: f64,
65    working: Vec<Order>,
66    finished: Vec<Order>,
67    executions: Vec<Execution>,
68    /// Every bracket exit, and the exits of the same order it acts on when it fills: a take
69    /// profit's stop loss, which it shrinks, and a stop loss's take profits, which it cancels.
70    exits: HashMap<OrderId, Vec<OrderId>>,
71    positions: HashMap<String, Position>,
72    last: HashMap<String, f64>,
73    balance_history: Vec<BalanceChange>,
74    journal: Vec<JournalEntry>,
75}
76
77/// Describes an order in the journal, such as "buy 2 AAPL limit 185".
78fn describe(request: &OrderRequest) -> String {
79    let side = match request.side {
80        OrderSide::Buy => "buy",
81        OrderSide::Sell => "sell",
82    };
83    let kind = match request.order_type {
84        OrderType::Market => "at market".to_owned(),
85        OrderType::Limit(limit) => format!("limit {limit}"),
86        OrderType::Stop(stop) => format!("stop {stop}"),
87        OrderType::StopLimit { stop, limit } => format!("stop {stop} limit {limit}"),
88    };
89    format!("{side} {} {} {kind}", request.quantity, request.symbol)
90}
91
92impl PaperAccount {
93    /// An empty account holding `balance` in `currency`, without leverage or commission.
94    ///
95    /// # Panics
96    ///
97    /// Panics if `balance` is NaN or infinite.
98    pub fn new(currency: impl Into<String>, balance: f64) -> Self {
99        assert!(balance.is_finite(), "starting balance must be finite");
100        let state = State {
101            balance,
102            ..State::default()
103        };
104        Self {
105            currency: currency.into(),
106            initial: balance,
107            leverage: 1.0,
108            commission: 0.0,
109            state: Mutex::new(state),
110        }
111    }
112
113    /// Sets the leverage: margin is the market value divided by it. Values below 1 count as 1.
114    pub fn leverage(mut self, leverage: f64) -> Self {
115        self.leverage = if leverage.is_finite() {
116            leverage.max(1.0)
117        } else {
118            1.0
119        };
120        self
121    }
122
123    /// Sets the commission each fill pays, as a fraction of its value, such as 0.001 for 0.1%.
124    pub fn commission(mut self, rate: f64) -> Self {
125        self.commission = if rate.is_finite() { rate.max(0.0) } else { 0.0 };
126        self
127    }
128
129    fn lock(&self) -> std::sync::MutexGuard<'_, State> {
130        self.state
131            .lock()
132            .unwrap_or_else(|poisoned| poisoned.into_inner())
133    }
134
135    /// Takes `price` as the last price of `symbol`: fills the working orders it reaches and
136    /// revalues the position. Non-finite prices are ignored.
137    pub fn set_price(&self, symbol: &str, price: f64) {
138        let mut state = self.lock();
139        if price.is_finite() && state.last.insert(symbol.to_owned(), price) != Some(price) {
140            state.reach(self, symbol, price);
141            if let Some(mut position) = state.positions.remove(symbol) {
142                state.value(self, &mut position);
143                state.positions.insert(symbol.to_owned(), position.clone());
144                state.push(AccountEvent::Position(position));
145                state.push(state.account(self));
146            }
147        }
148    }
149
150    /// Uses `time` for orders, executions, balance changes and journal entries during replay.
151    /// Set it to the revealed source bar's time before feeding its price. `None` restores the
152    /// live UTC clock.
153    pub fn set_replay_time(&self, time: Option<DateTime<Utc>>) {
154        self.lock().replay_time = time;
155    }
156
157    /// Returns the account to its starting balance without orders, positions or history,
158    /// retaining the host's prices and replay clock. The terminal shows the fresh account once
159    /// it connects again, such as through
160    /// `ui::trading::Trading::set_service` with the `ui` feature.
161    pub fn reset(&self) {
162        let mut state = self.lock();
163        let last = std::mem::take(&mut state.last);
164        *state = State {
165            balance: self.initial,
166            replay_time: state.replay_time,
167            last,
168            ..State::default()
169        };
170    }
171}
172
173impl State {
174    fn now(&self) -> DateTime<Utc> {
175        self.replay_time.unwrap_or_else(Utc::now)
176    }
177
178    fn push(&self, event: AccountEvent) {
179        if let Some(events) = &self.events {
180            events.push(event);
181        }
182    }
183
184    fn log(&mut self, text: String) {
185        let entry = JournalEntry::new(self.now(), text);
186        self.push(AccountEvent::Journal(entry.clone()));
187        if self.journal.len() == 1_000 {
188            self.journal.remove(0);
189        }
190        self.journal.push(entry);
191    }
192
193    fn book(&mut self, amount: f64, action: String) {
194        if amount == 0.0 {
195            return;
196        }
197        self.balance += amount;
198        let change = BalanceChange::new(self.now(), self.balance, amount, action);
199        self.push(AccountEvent::Balance(change.clone()));
200        if self.balance_history.len() == 1_000 {
201            self.balance_history.remove(0);
202        }
203        self.balance_history.push(change);
204    }
205
206    /// Values `position` at the symbol's last price.
207    fn value(&self, terms: &PaperAccount, position: &mut Position) {
208        let last = self
209            .last
210            .get(&position.symbol)
211            .copied()
212            .unwrap_or(position.average_price);
213        let size = position.quantity.abs();
214        position.last_price = Some(last);
215        position.unrealized_pnl = Some((last - position.average_price) * position.quantity);
216        position.trade_value = Some(size * position.average_price);
217        position.market_value = Some(size * last);
218        position.leverage = Some(terms.leverage);
219        position.margin = Some(size * last.abs() / terms.leverage);
220    }
221
222    fn account(&self, terms: &PaperAccount) -> AccountEvent {
223        let mut account = Account::new(
224            &terms.currency,
225            self.balance,
226            Capabilities {
227                market: true,
228                limit: true,
229                stop: true,
230                stop_limit: true,
231                brackets: true,
232                take_profit_levels: true,
233                close: true,
234                reverse: true,
235                preview: false,
236                modify: true,
237            },
238        );
239        let (equity, margin) = self.equity_and_margin(terms);
240        let (orders_margin, commissions) = self.reservations(terms, None);
241        account.equity = Some(equity);
242        account.realized_pnl = Some(self.balance - terms.initial);
243        account.margin = Some(margin);
244        account.orders_margin = Some(orders_margin);
245        account.available_funds = Some(equity - margin - orders_margin - commissions);
246        AccountEvent::Account(account)
247    }
248
249    fn equity_and_margin(&self, terms: &PaperAccount) -> (f64, f64) {
250        let mut equity = self.balance;
251        let mut margin = 0.0;
252        for position in self.positions.values() {
253            let price = self
254                .last
255                .get(&position.symbol)
256                .copied()
257                .unwrap_or(position.average_price);
258            equity += (price - position.average_price) * position.quantity;
259            margin += position.quantity.abs() * price.abs() / terms.leverage;
260        }
261        (equity, margin)
262    }
263
264    fn reservations(&self, terms: &PaperAccount, replacing: Option<&OrderId>) -> (f64, f64) {
265        let value: f64 = self
266            .working
267            .iter()
268            .filter(|order| {
269                Some(&order.id) != replacing
270                    && !self.exits.contains_key(&order.id)
271                    && order.request.position_bracket.is_none()
272            })
273            .map(|order| {
274                let price = match order.request.order_type {
275                    OrderType::Limit(price) | OrderType::Stop(price) => price,
276                    OrderType::StopLimit { limit, .. } => limit,
277                    _ => self.last.get(&order.request.symbol).copied().unwrap_or(0.0),
278                };
279                order.request.quantity * price.abs()
280            })
281            .sum();
282        (value / terms.leverage, value * terms.commission)
283    }
284
285    /// Projects this request against the whole account, excluding the order it replaces.
286    fn buying_power(
287        &self,
288        terms: &PaperAccount,
289        request: &OrderRequest,
290        replacing: Option<&OrderId>,
291    ) -> Result<(), FeedError> {
292        let held = self
293            .positions
294            .get(&request.symbol)
295            .map_or(0.0, |p| p.quantity);
296        let signed = if request.side == OrderSide::Buy {
297            request.quantity
298        } else {
299            -request.quantity
300        };
301        let after = held + signed;
302        if request.position_bracket.is_some()
303            || replacing.is_some_and(|id| self.exits.contains_key(id))
304        {
305            return Ok(());
306        }
307        let last = self.last[&request.symbol];
308        let reached =
309            |order_type: OrderType| order_type.fill_price(request.side, last, last).is_some();
310        let immediate = reached(request.order_type)
311            && match request.order_type {
312                OrderType::StopLimit { limit, .. } => reached(OrderType::Limit(limit)),
313                _ => true,
314            };
315        if immediate && after.abs() < held.abs() {
316            return Ok(());
317        }
318        let (equity, mut margin) = self.equity_and_margin(terms);
319        let price = match request.order_type {
320            _ if immediate => last,
321            OrderType::Market => last,
322            OrderType::Limit(price) | OrderType::Stop(price) => price,
323            OrderType::StopLimit { limit, .. } => limit,
324        };
325        let (reserved, commissions) = self.reservations(terms, replacing);
326        let value = request.quantity * price.abs();
327        if immediate {
328            margin += (after.abs() - held.abs()) * last.abs() / terms.leverage;
329        } else {
330            margin += value / terms.leverage;
331        }
332        let needed = margin + reserved + commissions + value * terms.commission;
333        if !needed.is_finite() || !equity.is_finite() || needed > equity {
334            return Err(FeedError::Other(format!(
335                "Insufficient buying power: {needed:.2} {} needed, {equity:.2} available",
336                terms.currency
337            )));
338        }
339        Ok(())
340    }
341
342    fn id(&mut self) -> OrderId {
343        self.next_id += 1;
344        OrderId(self.next_id.to_string())
345    }
346
347    fn finish(&mut self, mut order: Order, status: OrderStatus) {
348        order.status = status;
349        order.closed = Some(self.now());
350        self.push(AccountEvent::Order(order.clone()));
351        if self.finished.len() == 1_000 {
352            self.finished.remove(0);
353        }
354        self.finished.push(order);
355    }
356
357    /// Fills `order` in full at `price`: updates the net position, books the realised profit
358    /// and the commission, and turns brackets into exit orders.
359    fn fill(&mut self, terms: &PaperAccount, mut order: Order, price: f64) {
360        let request = order.request.clone();
361        let signed = match request.side {
362            OrderSide::Buy => request.quantity,
363            OrderSide::Sell => -request.quantity,
364        };
365        let mut position = self
366            .positions
367            .remove(&request.symbol)
368            .unwrap_or_else(|| Position::new(&request.symbol, 0.0, price));
369        let held = position.quantity;
370        let mut realized = 0.0;
371        if held * signed >= 0.0 {
372            position.average_price =
373                (held * position.average_price + signed * price) / (held + signed);
374        } else {
375            let closed = signed.abs().min(held.abs());
376            realized = closed * (price - position.average_price) * held.signum();
377            if signed.abs() > held.abs() {
378                position.average_price = price;
379            }
380        }
381        let remaining = held + signed;
382        position.quantity =
383            if remaining.abs() <= rounding_tolerance(held.abs().max(signed.abs()), 1e-9) {
384                0.0
385            } else {
386                remaining
387            };
388        self.value(terms, &mut position);
389        if position.quantity != 0.0 {
390            self.positions
391                .insert(request.symbol.clone(), position.clone());
392        }
393
394        order.filled = request.quantity;
395        order.fill_price = Some(price);
396        let execution = Execution::new(
397            order.id.clone(),
398            self.now(),
399            request.side,
400            price,
401            request.quantity,
402        );
403        let id = order.id.clone();
404        self.finish(order, OrderStatus::Filled);
405        self.push(AccountEvent::Execution(execution.clone()));
406        if self.executions.len() == 1_000 {
407            self.executions.remove(0);
408        }
409        self.executions.push(execution);
410        self.push(AccountEvent::Position(position.clone()));
411        self.log(format!(
412            "Order {id} filled: {} at {price}",
413            describe(&request)
414        ));
415        self.book(
416            realized,
417            format!(
418                "Profit or loss of {} {} closed by order {id}",
419                (held.abs()).min(request.quantity),
420                request.symbol
421            ),
422        );
423        self.book(
424            -request.quantity * price.abs() * terms.commission,
425            format!("Commission for order {id}"),
426        );
427        if held != 0.0 && position.quantity == 0.0 {
428            self.log(format!("Position {} closed", request.symbol));
429        }
430        if let Some(partners) = self.exits.remove(&id) {
431            for other in partners {
432                if let Some(list) = self.exits.get_mut(&other) {
433                    list.retain(|exit| *exit != id);
434                }
435                let working = self.working.iter_mut().find(|order| order.id == other);
436                match working {
437                    Some(stop_loss)
438                        if request.position_bracket == Some(PositionBracket::TakeProfit)
439                            && stop_loss.request.quantity - request.quantity
440                                > rounding_tolerance(stop_loss.request.quantity, 1e-9) =>
441                    {
442                        stop_loss.request.quantity -= request.quantity;
443                        let stop_loss = stop_loss.clone();
444                        self.push(AccountEvent::Order(stop_loss));
445                    }
446                    _ => {
447                        self.cancel_working(&other);
448                    }
449                }
450            }
451        }
452        // Exits may only reduce the remaining position, including after a manual close or reverse.
453        for mut exit in self.working.clone() {
454            if exit.request.symbol != request.symbol
455                || (!self.exits.contains_key(&exit.id) && exit.request.position_bracket.is_none())
456            {
457                continue;
458            }
459            if !exit.request.side.reduces(position.quantity) {
460                self.cancel_working(&exit.id);
461            } else if exit.request.quantity > position.quantity.abs() {
462                exit.request.quantity = position.quantity.abs();
463                if let Some(working) = self
464                    .working
465                    .iter_mut()
466                    .find(|working| working.id == exit.id)
467                {
468                    *working = exit.clone();
469                }
470                self.push(AccountEvent::Order(exit));
471            }
472        }
473
474        let protected = if position.quantity.signum() == signed.signum() {
475            request.quantity.min(position.quantity.abs())
476        } else {
477            0.0
478        };
479        let exit = |price, quantity, order_type: fn(f64) -> OrderType| {
480            let side = match request.side {
481                OrderSide::Buy => OrderSide::Sell,
482                OrderSide::Sell => OrderSide::Buy,
483            };
484            let mut exit = OrderRequest::new(&request.symbol, side, quantity, order_type(price));
485            exit.time_in_force = TimeInForce::GoodTillCancelled;
486            exit.position_bracket = Some(match exit.order_type {
487                OrderType::Limit(_) => PositionBracket::TakeProfit,
488                _ => PositionBracket::StopLoss,
489            });
490            exit
491        };
492        // Levels close their own quantity, the first ones first when less than the order opened.
493        let mut left = protected;
494        let take_profits = request
495            .take_profit_levels
496            .iter()
497            .map(|level| {
498                let share = level.quantity.min(left);
499                left -= share;
500                (level.price, share)
501            })
502            .chain(request.take_profit.map(|price| (price, protected)))
503            .filter(|(_, quantity)| *quantity > rounding_tolerance(protected, 1e-9))
504            .map(|(price, quantity)| exit(price, quantity, OrderType::Limit));
505        let stop_loss = request
506            .stop_loss
507            .filter(|_| protected > 0.0)
508            .map(|price| exit(price, protected, OrderType::Stop));
509        let exits: Vec<_> = take_profits
510            .chain(stop_loss)
511            .map(|exit| {
512                let id = self.id();
513                let mut order = Order::new(id, exit, OrderStatus::Working);
514                order.placed = Some(self.now());
515                order
516            })
517            .collect();
518        let ids = |bracket| {
519            exits
520                .iter()
521                .filter(|exit| exit.request.position_bracket == Some(bracket))
522                .map(|exit| exit.id.clone())
523                .collect::<Vec<_>>()
524        };
525        let (take_profits, stop_losses) = (
526            ids(PositionBracket::TakeProfit),
527            ids(PositionBracket::StopLoss),
528        );
529        for exit in exits {
530            let partners = match exit.request.position_bracket {
531                Some(PositionBracket::StopLoss) => take_profits.clone(),
532                _ => stop_losses.clone(),
533            };
534            self.exits.insert(exit.id.clone(), partners);
535            self.push(AccountEvent::Order(exit.clone()));
536            self.log(format!(
537                "Exit order {} placed: {}",
538                exit.id,
539                describe(&exit.request)
540            ));
541            self.working.push(exit);
542        }
543        self.push(self.account(terms));
544    }
545
546    fn cancel_working(&mut self, id: &OrderId) -> bool {
547        let Some(index) = self.working.iter().position(|order| &order.id == id) else {
548            return false;
549        };
550        let order = self.working.remove(index);
551        for other in self.exits.remove(id).into_iter().flatten() {
552            if let Some(list) = self.exits.get_mut(&other) {
553                list.retain(|exit| exit != id);
554            }
555        }
556        self.log(format!(
557            "Order {id} cancelled: {}",
558            describe(&order.request)
559        ));
560        self.finish(order, OrderStatus::Cancelled);
561        true
562    }
563
564    /// Fills the working orders of `symbol` that `price` reached.
565    fn reach(&mut self, terms: &PaperAccount, symbol: &str, price: f64) {
566        let mut index = 0;
567        while index < self.working.len() {
568            let order = &mut self.working[index];
569            let fill = if order.request.symbol == symbol {
570                order
571                    .request
572                    .order_type
573                    .fill_price(order.request.side, price, price)
574            } else {
575                None
576            };
577            if let (Some(_), OrderType::StopLimit { limit, .. }) = (fill, order.request.order_type)
578            {
579                // Triggered: it works on as a limit order, which may fill at once.
580                order.request.order_type = OrderType::Limit(limit);
581                let order = order.clone();
582                self.push(AccountEvent::Order(order));
583                continue;
584            }
585            if let Some(fill) = fill {
586                let order = self.working.remove(index);
587                let mut request = order.request.clone();
588                request.order_type = OrderType::Market;
589                match self.buying_power(terms, &request, Some(&order.id)) {
590                    Ok(()) => self.fill(terms, order, fill),
591                    Err(error) => {
592                        self.log(format!("Order {} rejected: {error}", order.id));
593                        self.finish(order, OrderStatus::Rejected(error.to_string()));
594                        self.push(self.account(terms));
595                    }
596                }
597                index = 0;
598            } else {
599                index += 1;
600            }
601        }
602    }
603}
604
605/// Reject malformed requests before they can change the account.
606fn validate(order: &OrderRequest, last: f64) -> Result<(), FeedError> {
607    let price = match order.order_type {
608        OrderType::Market => last,
609        OrderType::Limit(price) | OrderType::Stop(price) => price,
610        OrderType::StopLimit { stop, limit } if stop.is_finite() => limit,
611        OrderType::StopLimit { .. } => f64::NAN,
612    };
613    let buy = order.side == OrderSide::Buy;
614    let no_profit = |p: f64| !p.is_finite() || if buy { p <= price } else { p >= price };
615    let levels = &order.take_profit_levels;
616    let bad_levels = !levels.is_empty()
617        && (order.take_profit.is_some()
618            || levels.iter().any(|level| {
619                no_profit(level.price) || !level.quantity.is_finite() || level.quantity <= 0.0
620            })
621            || (levels.iter().map(|level| level.quantity).sum::<f64>() - order.quantity).abs()
622                > rounding_tolerance(order.quantity, 1e-9));
623    if !order.quantity.is_finite()
624        || order.quantity <= 0.0
625        || !price.is_finite()
626        || !(order.quantity * price).is_finite()
627        || order.take_profit.is_some_and(no_profit)
628        || bad_levels
629        || order
630            .stop_loss
631            .is_some_and(|p| !p.is_finite() || if buy { p >= price } else { p <= price })
632    {
633        return Err(FeedError::Other(
634            "invalid quantity, price or brackets".into(),
635        ));
636    }
637    if order.time_in_force != TimeInForce::GoodTillCancelled {
638        return Err(FeedError::NotSupported);
639    }
640    Ok(())
641}
642
643impl Broker for PaperAccount {
644    fn connect(&self, events: Events<AccountEvent>) {
645        let mut state = self.lock();
646        state.events = Some(events);
647        state.push(state.account(self));
648        for order in state.working.iter().chain(&state.finished) {
649            state.push(AccountEvent::Order(order.clone()));
650        }
651        for position in state.positions.values() {
652            state.push(AccountEvent::Position(position.clone()));
653        }
654        for execution in &state.executions {
655            state.push(AccountEvent::Execution(execution.clone()));
656        }
657        for change in &state.balance_history {
658            state.push(AccountEvent::Balance(change.clone()));
659        }
660        for entry in &state.journal {
661            state.push(AccountEvent::Journal(entry.clone()));
662        }
663    }
664
665    fn place(&self, order: OrderRequest, reply: Reply<OrderId>) {
666        let mut state = self.lock();
667        let Some(&last) = state.last.get(&order.symbol) else {
668            return reply.send(Err(FeedError::SymbolNotFound(order.symbol)));
669        };
670        if let Err(error) = validate(&order, last) {
671            return reply.send(Err(error));
672        }
673        let id = state.id();
674        reply.send(Ok(id.clone()));
675        state.log(format!("Order {id} placed: {}", describe(&order)));
676        let mut order = Order::new(id, order, OrderStatus::Working);
677        order.placed = Some(state.now());
678        let held = state
679            .positions
680            .get(&order.request.symbol)
681            .map_or(0.0, |p| p.quantity);
682        let request = &order.request;
683        if request.position_bracket.is_some()
684            && (held == 0.0
685                || (request.side == OrderSide::Buy) == (held > 0.0)
686                || request.quantity > held.abs())
687        {
688            state.finish(
689                order,
690                OrderStatus::Rejected("A position exit must reduce the open position".into()),
691            );
692            return;
693        }
694        if let Err(error) = state.buying_power(self, request, None) {
695            state.finish(order, OrderStatus::Rejected(error.to_string()));
696            return;
697        }
698        if order.request.order_type == OrderType::Market {
699            state.fill(self, order, last);
700        } else {
701            let symbol = order.request.symbol.clone();
702            state.push(AccountEvent::Order(order.clone()));
703            state.working.push(order);
704            // A marketable limit or a reached stop fills at once.
705            state.reach(self, &symbol, last);
706            state.push(state.account(self));
707        }
708    }
709
710    fn cancel(&self, id: OrderId, reply: Reply<()>) {
711        let mut state = self.lock();
712        if state.cancel_working(&id) {
713            reply.send(Ok(()));
714            state.push(state.account(self));
715        } else {
716            reply.send(Err(FeedError::Other(format!(
717                "order {id} is no longer working"
718            ))));
719        }
720    }
721
722    fn modify(&self, id: OrderId, order: OrderRequest, reply: Reply<()>) {
723        let mut state = self.lock();
724        let Some(working) = state.working.iter().find(|working| working.id == id) else {
725            return reply.send(Err(FeedError::Other(format!(
726                "order {id} is no longer working"
727            ))));
728        };
729        if order.symbol != working.request.symbol
730            || order.side != working.request.side
731            || order.position_bracket != working.request.position_bracket
732        {
733            return reply.send(Err(FeedError::Other(
734                "an order's symbol, side and position bracket cannot change".into(),
735            )));
736        }
737        let last = state.last[&order.symbol];
738        if let Err(error) = validate(&order, last) {
739            return reply.send(Err(error));
740        }
741        if (state.exits.contains_key(&id) || order.position_bracket.is_some())
742            && (state.positions.get(&order.symbol).is_none_or(|position| {
743                order.quantity > position.quantity.abs()
744                    || (order.side == OrderSide::Buy) == (position.quantity > 0.0)
745            }) || order.take_profit.is_some()
746                || !order.take_profit_levels.is_empty()
747                || order.stop_loss.is_some())
748        {
749            return reply.send(Err(FeedError::Other(
750                "an exit may only reduce its position".into(),
751            )));
752        }
753        if let Err(error) = state.buying_power(self, &order, Some(&id)) {
754            return reply.send(Err(error));
755        }
756        let working = state
757            .working
758            .iter_mut()
759            .find(|working| working.id == id)
760            .unwrap();
761        working.request = order;
762        let (working, symbol) = (working.clone(), working.request.symbol.clone());
763        reply.send(Ok(()));
764        state.push(AccountEvent::Order(working.clone()));
765        state.log(format!(
766            "Order {id} changed: {}",
767            describe(&working.request)
768        ));
769        // A limit moved through the market or a stop moved past it fills at once.
770        if let Some(&last) = state.last.get(&symbol) {
771            state.reach(self, &symbol, last);
772        }
773        state.push(state.account(self));
774    }
775
776    fn rules(&self, symbol: &str, reply: Reply<TradingRules>) {
777        let mut rules = TradingRules::new(symbol);
778        rules.time_in_force = vec![TimeInForce::GoodTillCancelled];
779        rules.leverage = Some(self.leverage);
780        reply.send(Ok(rules));
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787    use crate::data::inbox::Inbox;
788    use crate::model::TakeProfitLevel;
789    use crate::services::events::Listener;
790    use std::sync::Arc;
791
792    fn snapshot(paper: &PaperAccount) -> Vec<AccountEvent> {
793        let inbox = Arc::new(Inbox::default());
794        let (_listener, events) = Listener::new(&inbox, 2);
795        paper.connect(events);
796        inbox
797            .drain()
798            .answers
799            .into_iter()
800            .map(|(_, event)| *event.downcast::<AccountEvent>().unwrap())
801            .collect()
802    }
803
804    fn place(paper: &PaperAccount, mut request: OrderRequest) -> Result<OrderId, FeedError> {
805        request.time_in_force = TimeInForce::GoodTillCancelled;
806        let inbox = Arc::new(Inbox::default());
807        paper.place(request, inbox.reply(0));
808        *inbox
809            .drain()
810            .answers
811            .pop()
812            .unwrap()
813            .1
814            .downcast::<Result<OrderId, FeedError>>()
815            .unwrap()
816    }
817
818    #[test]
819    fn account_figures_follow_adds_partial_closes_and_reversals() {
820        for direction in [1.0, -1.0] {
821            let paper = PaperAccount::new("USD", 10_000.0)
822                .leverage(10.0)
823                .commission(0.001);
824            let time = DateTime::from_timestamp(1_704_067_200, 0).unwrap();
825            paper.set_replay_time(Some(time));
826            let mut balance = 10_000.0;
827            // Price, signed fill, remaining quantity, average, gross realised profit.
828            for (price, fill, quantity, average, realized) in [
829                (100.0, 2.0_f64, 2.0, 100.0, 0.0),
830                (130.0, 1.0, 3.0, 110.0, 0.0),
831                (140.0, -1.0, 2.0, 110.0, 30.0),
832                (120.0, -4.0, -2.0, 120.0, 20.0),
833                (-10.0, 1.0, -1.0, 120.0, 130.0),
834            ] {
835                paper.set_price("SYM", price);
836                let side = if fill * direction > 0.0 {
837                    OrderSide::Buy
838                } else {
839                    OrderSide::Sell
840                };
841                place(
842                    &paper,
843                    OrderRequest::new("SYM", side, fill.abs(), OrderType::Market),
844                )
845                .unwrap();
846                balance += realized * direction - fill.abs() * price.abs() * 0.001;
847                let events = snapshot(&paper);
848                assert!(events.iter().all(|event| match event {
849                    AccountEvent::Order(order) => {
850                        order.placed == Some(time) && order.closed == Some(time)
851                    }
852                    AccountEvent::Execution(fill) => fill.time == time,
853                    AccountEvent::Balance(change) => change.time == time,
854                    AccountEvent::Journal(entry) => entry.time == time,
855                    _ => true,
856                }));
857                let account = events
858                    .iter()
859                    .find_map(|e| {
860                        if let AccountEvent::Account(a) = e {
861                            Some(a)
862                        } else {
863                            None
864                        }
865                    })
866                    .unwrap();
867                let position = events
868                    .iter()
869                    .find_map(|e| {
870                        if let AccountEvent::Position(p) = e {
871                            Some(p)
872                        } else {
873                            None
874                        }
875                    })
876                    .unwrap();
877                let near = |a: f64, b: f64| assert!((a - b).abs() < 1e-9, "{a} != {b}");
878                near(position.quantity, quantity * direction);
879                near(position.average_price, average);
880                near(account.balance, balance);
881                near(account.realized_pnl.unwrap(), balance - 10_000.0);
882                let open = (price - average) * quantity * direction;
883                let margin = (quantity * price).abs() / 10.0;
884                near(position.unrealized_pnl.unwrap(), open);
885                near(position.margin.unwrap(), margin);
886                near(account.margin.unwrap(), margin);
887                near(account.equity.unwrap(), balance + open);
888                near(account.available_funds.unwrap(), balance + open - margin);
889            }
890            // Close the remaining unit, then 0.1 + 0.2 with 0.3 on either side.
891            // One more unit scaled out in ten tenths is also flat: 1 - 10 * 0.1 = 0.
892            for fill in [
893                direction,
894                0.1 * direction,
895                0.2 * direction,
896                -0.3 * direction,
897                direction,
898            ]
899            .into_iter()
900            .chain([-0.1 * direction; 10])
901            {
902                place(
903                    &paper,
904                    OrderRequest::new(
905                        "SYM",
906                        if fill > 0.0 {
907                            OrderSide::Buy
908                        } else {
909                            OrderSide::Sell
910                        },
911                        fill.abs(),
912                        OrderType::Market,
913                    ),
914                )
915                .unwrap();
916            }
917            assert!(!paper.lock().positions.contains_key("SYM"));
918            paper.set_price("SYM", 100.0);
919            place(
920                &paper,
921                OrderRequest::new("SYM", OrderSide::Buy, 1.0, OrderType::Limit(90.0)),
922            )
923            .unwrap();
924            let events = snapshot(&paper);
925            let account = events
926                .iter()
927                .find_map(|e| {
928                    if let AccountEvent::Account(a) = e {
929                        Some(a)
930                    } else {
931                        None
932                    }
933                })
934                .unwrap();
935            assert_eq!(account.orders_margin, Some(9.0));
936            assert_eq!(
937                account.available_funds,
938                Some(account.equity.unwrap() - account.margin.unwrap() - 9.0 - 0.09)
939            );
940            assert_eq!(
941                events
942                    .iter()
943                    .filter(|e| matches!(e, AccountEvent::Execution(_)))
944                    .count(),
945                20
946            );
947            paper.reset();
948            let events = snapshot(&paper);
949            assert_eq!(events.len(), 1);
950            assert!(matches!(&events[0], AccountEvent::Account(a) if a.balance == 10_000.0));
951            for replay in [true, false] {
952                if !replay {
953                    paper.set_replay_time(None);
954                }
955                let before = Utc::now();
956                place(
957                    &paper,
958                    OrderRequest::new("SYM", OrderSide::Buy, 1.0, OrderType::Market),
959                )
960                .unwrap();
961                let events = snapshot(&paper);
962                let fill = events
963                    .iter()
964                    .rev()
965                    .find_map(|event| match event {
966                        AccountEvent::Execution(fill) => Some(fill),
967                        _ => None,
968                    })
969                    .unwrap();
970                if replay {
971                    assert_eq!(fill.time, time, "reset must keep the host's replay clock");
972                } else {
973                    assert!(
974                        fill.time >= before,
975                        "leaving replay restores the live clock"
976                    );
977                }
978            }
979        }
980    }
981
982    #[test]
983    fn bracket_exits_only_reduce_the_remaining_position_and_cancel_their_pair() {
984        for closed in [0.0, 1.0, 2.0, 4.0] {
985            for (attached, close_brackets) in [(true, false), (true, true), (false, false)] {
986                let paper = PaperAccount::new("USD", 10_000.0);
987                paper.set_price("SYM", 100.0);
988                let mut entry = OrderRequest::new("SYM", OrderSide::Buy, 2.0, OrderType::Market);
989                if attached {
990                    entry.take_profit = Some(105.0);
991                    entry.stop_loss = Some(95.0);
992                }
993                place(&paper, entry).unwrap();
994                if !attached {
995                    for (kind, role) in [
996                        (OrderType::Limit(105.0), PositionBracket::TakeProfit),
997                        (OrderType::Stop(95.0), PositionBracket::StopLoss),
998                    ] {
999                        let mut exit = OrderRequest::new("SYM", OrderSide::Sell, 2.0, kind);
1000                        exit.position_bracket = Some(role);
1001                        place(&paper, exit).unwrap();
1002                    }
1003                }
1004                if closed > 0.0 {
1005                    let mut close =
1006                        OrderRequest::new("SYM", OrderSide::Sell, closed, OrderType::Market);
1007                    if close_brackets {
1008                        close.take_profit = Some(90.0);
1009                        close.stop_loss = Some(110.0);
1010                    }
1011                    place(&paper, close).unwrap();
1012                }
1013                let events = snapshot(&paper);
1014                let exits: Vec<_> = events
1015                    .iter()
1016                    .filter_map(|e| match e {
1017                        AccountEvent::Order(o) if o.status == OrderStatus::Working => Some(o),
1018                        _ => None,
1019                    })
1020                    .collect();
1021                let expected = if closed < 2.0 {
1022                    2.0 - closed
1023                } else if close_brackets {
1024                    closed - 2.0
1025                } else {
1026                    0.0
1027                };
1028                assert_eq!(exits.len(), if expected > 0.0 { 2 } else { 0 });
1029                assert!(exits.iter().all(|o| o.request.quantity == expected));
1030                paper.set_price("SYM", 106.0);
1031                paper.set_price("SYM", 94.0);
1032                paper.set_price("SYM", 111.0);
1033                let events = snapshot(&paper);
1034                let position = events.iter().find_map(|e| {
1035                    if let AccountEvent::Position(p) = e {
1036                        Some(p.quantity)
1037                    } else {
1038                        None
1039                    }
1040                });
1041                assert_eq!(
1042                    position.unwrap_or(0.0),
1043                    if closed == 4.0 && !close_brackets {
1044                        -2.0
1045                    } else {
1046                        0.0
1047                    }
1048                );
1049                assert!(!events.iter().any(
1050                    |e| matches!(e, AccountEvent::Order(o) if o.status == OrderStatus::Working)
1051                ));
1052                assert!(events.iter().any(
1053                    |e| matches!(e, AccountEvent::Order(o) if o.status == OrderStatus::Cancelled)
1054                ));
1055            }
1056        }
1057    }
1058
1059    /// Levels close their own quantity, and the first ones first when the fill opened less. A
1060    /// take profit shrinks the stop loss so that it protects what is left of the order and no
1061    /// more, and cancels it with the last level; the stop loss cancels the levels. A regression
1062    /// leaves the rest unprotected, closes units that were never part of the order, or keeps
1063    /// levels working on a position they no longer belong to.
1064    #[test]
1065    fn take_profit_levels_close_their_quantity_and_shrink_the_stop_loss() {
1066        let limit = |price, quantity| (OrderType::Limit(price), quantity);
1067        let stop = |price, quantity| (OrderType::Stop(price), quantity);
1068        // (held before, exits, price, exits, price, exits, position, balance)
1069        for (held, entered, first, partly, second, last, position, balance) in [
1070            // 1 at 106 earns 6 and 2 at 94 lose 12.
1071            (
1072                0.0_f64,
1073                vec![limit(105.0, 1.0), limit(110.0, 2.0), stop(95.0, 3.0)],
1074                106.0,
1075                vec![limit(110.0, 2.0), stop(95.0, 2.0)],
1076                94.0,
1077                vec![],
1078                0.0,
1079                9_994.0,
1080            ),
1081            // The same, on top of 5 units the order's exits must leave alone.
1082            (
1083                5.0,
1084                vec![limit(105.0, 1.0), limit(110.0, 2.0), stop(95.0, 3.0)],
1085                106.0,
1086                vec![limit(110.0, 2.0), stop(95.0, 2.0)],
1087                94.0,
1088                vec![],
1089                5.0,
1090                9_994.0,
1091            ),
1092            // Closing a short of 1 opens 2 of the 3, which both levels close at 111 for 11 each.
1093            (
1094                -1.0,
1095                vec![limit(105.0, 1.0), limit(110.0, 1.0), stop(95.0, 2.0)],
1096                111.0,
1097                vec![],
1098                111.0,
1099                vec![],
1100                0.0,
1101                10_022.0,
1102            ),
1103        ] {
1104            let paper = PaperAccount::new("USD", 10_000.0);
1105            paper.set_price("SYM", 100.0);
1106            if held != 0.0 {
1107                let side = if held > 0.0 {
1108                    OrderSide::Buy
1109                } else {
1110                    OrderSide::Sell
1111                };
1112                let start = OrderRequest::new("SYM", side, held.abs(), OrderType::Market);
1113                place(&paper, start).unwrap();
1114            }
1115            let mut entry = OrderRequest::new("SYM", OrderSide::Buy, 3.0, OrderType::Market);
1116            entry.take_profit_levels = vec![
1117                TakeProfitLevel::new(105.0, 1.0),
1118                TakeProfitLevel::new(110.0, 2.0),
1119            ];
1120            entry.stop_loss = Some(95.0);
1121            place(&paper, entry).unwrap();
1122            let exits = || {
1123                let state = paper.lock();
1124                let exits = state.working.iter();
1125                exits
1126                    .map(|order| (order.request.order_type, order.request.quantity))
1127                    .collect::<Vec<_>>()
1128            };
1129            assert_eq!(exits(), entered, "held {held}");
1130            paper.set_price("SYM", first);
1131            assert_eq!(exits(), partly, "held {held}");
1132            paper.set_price("SYM", second);
1133            assert_eq!(exits(), last, "held {held}");
1134            let state = paper.lock();
1135            let quantity = state.positions.get("SYM").map_or(0.0, |p| p.quantity);
1136            assert_eq!(quantity, position, "held {held}");
1137            assert_eq!(state.balance, balance, "held {held}");
1138        }
1139    }
1140
1141    #[test]
1142    fn reconnect_keeps_recent_history_within_the_stream_limit() {
1143        let paper = PaperAccount::new("USD", 1_000_000.0).commission(0.001);
1144        paper.set_price("SYM", 100.0);
1145        for _ in 0..1_001 {
1146            place(
1147                &paper,
1148                OrderRequest::new("SYM", OrderSide::Buy, 1.0, OrderType::Market),
1149            )
1150            .unwrap();
1151        }
1152        let inbox = Arc::new(Inbox::default());
1153        let (listener, events) = Listener::new(&inbox, 2);
1154        paper.connect(events);
1155        assert!(listener.is_active());
1156        let events: Vec<_> = inbox
1157            .drain()
1158            .answers
1159            .into_iter()
1160            .map(|(_, e)| *e.downcast::<AccountEvent>().unwrap())
1161            .collect();
1162        for count in [
1163            events
1164                .iter()
1165                .filter(|e| matches!(e, AccountEvent::Order(_)))
1166                .count(),
1167            events
1168                .iter()
1169                .filter(|e| matches!(e, AccountEvent::Execution(_)))
1170                .count(),
1171            events
1172                .iter()
1173                .filter(|e| matches!(e, AccountEvent::Balance(_)))
1174                .count(),
1175            events
1176                .iter()
1177                .filter(|e| matches!(e, AccountEvent::Journal(_)))
1178                .count(),
1179        ] {
1180            assert_eq!(count, 1_000);
1181        }
1182    }
1183
1184    #[test]
1185    fn rejected_exposure_and_position_exits_do_not_fill() {
1186        let paper = PaperAccount::new("USD", 100.0).leverage(2.0);
1187        paper.set_price("SYM", 100.0);
1188        place(
1189            &paper,
1190            OrderRequest::new("SYM", OrderSide::Buy, 2.0, OrderType::Market),
1191        )
1192        .unwrap();
1193        place(
1194            &paper,
1195            OrderRequest::new("SYM", OrderSide::Buy, 1.0, OrderType::Market),
1196        )
1197        .unwrap();
1198        for (side, quantity) in [(OrderSide::Buy, 1.0), (OrderSide::Sell, 3.0)] {
1199            let mut exit = OrderRequest::new("SYM", side, quantity, OrderType::Limit(105.0));
1200            exit.position_bracket = Some(PositionBracket::TakeProfit);
1201            place(&paper, exit).unwrap();
1202        }
1203        let events = snapshot(&paper);
1204        assert_eq!(
1205            events
1206                .iter()
1207                .filter(|e| matches!(e, AccountEvent::Execution(_)))
1208                .count(),
1209            1
1210        );
1211        assert_eq!(events.iter().filter(|e| matches!(e, AccountEvent::Order(o) if matches!(o.status, OrderStatus::Rejected(_)))).count(), 3);
1212        // Reducing exposure remains possible even when the position exceeds buying power.
1213        paper.set_price("SYM", 200.0);
1214        place(
1215            &paper,
1216            OrderRequest::new("SYM", OrderSide::Sell, 1.0, OrderType::Market),
1217        )
1218        .unwrap();
1219        assert_eq!(paper.lock().positions["SYM"].quantity, 1.0);
1220    }
1221
1222    #[test]
1223    fn buying_power_counts_other_symbols_reservations_costs_and_modifications() {
1224        for (first, commission, quantity) in [
1225            (OrderType::Market, 0.0, 10.0),
1226            (OrderType::Limit(90.0), 0.0, 6.0),
1227            (OrderType::Market, 0.01, 5.0),
1228        ] {
1229            let paper = PaperAccount::new("USD", 1_000.0).commission(commission);
1230            paper.set_price("A", 100.0);
1231            paper.set_price("B", 100.0);
1232            // A holds 1,000 or 500 margin, or reserves 450. The new B order
1233            // needs 1,000, 600 or 500 plus 5 commission, respectively.
1234            place(
1235                &paper,
1236                OrderRequest::new(
1237                    "A",
1238                    OrderSide::Buy,
1239                    if quantity == 10.0 { 10.0 } else { 5.0 },
1240                    first,
1241                ),
1242            )
1243            .unwrap();
1244            let id = place(
1245                &paper,
1246                OrderRequest::new("B", OrderSide::Buy, quantity, OrderType::Market),
1247            )
1248            .unwrap();
1249            let state = paper.lock();
1250            assert!(!state.positions.contains_key("B"));
1251            assert!(state.finished.iter().any(|order| order.id == id && matches!(order.status, OrderStatus::Rejected(_))));
1252        }
1253        let paper = PaperAccount::new("USD", 1_000.0).commission(0.001);
1254        paper.set_price("A", 100.0);
1255        let mut request = OrderRequest::new("A", OrderSide::Buy, 1.0, OrderType::Limit(90.0));
1256        let id = place(&paper, request.clone()).unwrap();
1257        request.time_in_force = TimeInForce::GoodTillCancelled;
1258        let inbox = Arc::new(Inbox::default());
1259        for (quantity, price, accepted) in [
1260            (20.0, 90.0, false),
1261            (10.0, 90.0, true),
1262            (10.0, 91.0, true),
1263            (10.0, 100.0, false),
1264        ] {
1265            let before = snapshot(&paper);
1266            request.quantity = quantity;
1267            request.order_type = OrderType::Limit(price);
1268            paper.modify(id.clone(), request.clone(), inbox.reply(0));
1269            let result = *inbox
1270                .drain()
1271                .answers
1272                .pop()
1273                .unwrap()
1274                .1
1275                .downcast::<Result<(), FeedError>>()
1276                .unwrap();
1277            assert_eq!(
1278                result.is_ok(),
1279                accepted,
1280                "{quantity} at {price}: {result:?}"
1281            );
1282            if !accepted {
1283                assert_eq!(snapshot(&paper), before);
1284            }
1285        }
1286        paper.set_price("A", 90.0);
1287        let state = paper.lock();
1288        assert_eq!(state.positions["A"].quantity, 10.0);
1289        // 10 * 90 margin and 900 * 0.001 commission leave 99.10 available.
1290        assert_eq!(state.balance, 999.1);
1291        assert_eq!(state.positions["A"].margin, Some(900.0));
1292        drop(state);
1293
1294        // A gap through a stop cannot spend funds already held in another symbol.
1295        let paper = PaperAccount::new("USD", 1_000.0);
1296        paper.set_price("A", 100.0);
1297        paper.set_price("B", 100.0);
1298        place(
1299            &paper,
1300            OrderRequest::new("A", OrderSide::Buy, 5.0, OrderType::Market),
1301        )
1302        .unwrap();
1303        let id = place(
1304            &paper,
1305            OrderRequest::new("B", OrderSide::Buy, 4.0, OrderType::Stop(110.0)),
1306        )
1307        .unwrap();
1308        paper.set_price("B", 200.0);
1309        assert!(!paper.lock().positions.contains_key("B"));
1310        assert!(
1311            paper
1312                .lock()
1313                .finished
1314                .iter()
1315                .any(|order| order.id == id && matches!(order.status, OrderStatus::Rejected(_)))
1316        );
1317        // Unrealised losses reduce equity even though the cash balance has not changed.
1318        paper.set_price("A", 50.0);
1319        paper.set_price("B", 100.0);
1320        place(
1321            &paper,
1322            OrderRequest::new("B", OrderSide::Buy, 6.0, OrderType::Market),
1323        )
1324        .unwrap();
1325        assert!(!paper.lock().positions.contains_key("B"));
1326        // A marketable limit uses its actual, better fill price: 11 * 90 = 990.
1327        let paper = PaperAccount::new("USD", 1_000.0).commission(0.001);
1328        paper.set_price("A", 90.0);
1329        place(
1330            &paper,
1331            OrderRequest::new("A", OrderSide::Buy, 11.0, OrderType::Limit(100.0)),
1332        )
1333        .unwrap();
1334        assert_eq!(paper.lock().positions["A"].quantity, 11.0);
1335        assert_eq!(paper.lock().positions["A"].average_price, 90.0);
1336
1337        // Rejections during matching must report equity and margin at the new mark too.
1338        let paper = PaperAccount::new("USD", 1_000.0);
1339        paper.set_price("A", 100.0);
1340        place(
1341            &paper,
1342            OrderRequest::new("A", OrderSide::Buy, 5.0, OrderType::Market),
1343        )
1344        .unwrap();
1345        place(
1346            &paper,
1347            OrderRequest::new("A", OrderSide::Buy, 4.0, OrderType::Stop(110.0)),
1348        )
1349        .unwrap();
1350        let inbox = Arc::new(Inbox::default());
1351        let (_listener, events) = Listener::new(&inbox, 2);
1352        paper.connect(events);
1353        inbox.drain();
1354        paper.set_price("A", 200.0);
1355        let mut accounts = 0;
1356        for (_, event) in inbox.drain().answers {
1357            if let AccountEvent::Account(account) = *event.downcast::<AccountEvent>().unwrap() {
1358                accounts += 1;
1359                assert_eq!(account.equity, Some(1_500.0));
1360                assert_eq!(account.margin, Some(1_000.0));
1361            }
1362        }
1363        assert!(accounts > 0, "matching must publish the account");
1364    }
1365
1366    #[test]
1367    fn malformed_requests_and_exit_increases_leave_the_account_unchanged() {
1368        let paper = PaperAccount::new("USD", 10_000.0);
1369        paper.set_price("SYM", 100.0);
1370        for quantity in [0.0, -1.0, f64::NAN, f64::INFINITY] {
1371            assert!(
1372                place(
1373                    &paper,
1374                    OrderRequest::new("SYM", OrderSide::Buy, quantity, OrderType::Market)
1375                )
1376                .is_err()
1377            );
1378            assert_eq!(snapshot(&paper).len(), 1);
1379        }
1380        for kind in [
1381            OrderType::Limit(f64::NAN),
1382            OrderType::Stop(f64::INFINITY),
1383            OrderType::StopLimit {
1384                stop: f64::NAN,
1385                limit: 90.0,
1386            },
1387        ] {
1388            assert!(place(&paper, OrderRequest::new("SYM", OrderSide::Buy, 1.0, kind)).is_err());
1389        }
1390        let level = TakeProfitLevel::new;
1391        for (levels, take_profit) in [
1392            // They add up to 2 of 3.
1393            (vec![level(105.0, 1.0), level(110.0, 1.0)], None),
1394            (vec![level(95.0, 1.0), level(110.0, 2.0)], None),
1395            (vec![level(f64::NAN, 1.0), level(110.0, 2.0)], None),
1396            (vec![level(105.0, 0.0), level(110.0, 3.0)], None),
1397            (vec![level(105.0, 1.0), level(110.0, 2.0)], Some(110.0)),
1398        ] {
1399            let mut request = OrderRequest::new("SYM", OrderSide::Buy, 3.0, OrderType::Market);
1400            request.take_profit_levels = levels;
1401            request.take_profit = take_profit;
1402            assert!(place(&paper, request).is_err());
1403            assert_eq!(snapshot(&paper).len(), 1);
1404        }
1405        let mut entry = OrderRequest::new("SYM", OrderSide::Buy, 1.0, OrderType::Market);
1406        entry.take_profit = Some(110.0);
1407        place(&paper, entry).unwrap();
1408        let before = snapshot(&paper);
1409        let exit = before
1410            .iter()
1411            .find_map(|e| match e {
1412                AccountEvent::Order(o) if o.status == OrderStatus::Working => Some(o),
1413                _ => None,
1414            })
1415            .unwrap();
1416        let mut larger = exit.request.clone();
1417        larger.quantity = 2.0;
1418        // Valid for the exit's side and quantity: only the rule for exits refuses it.
1419        let mut with_level = exit.request.clone();
1420        with_level.take_profit_levels = vec![level(105.0, 1.0)];
1421        for changed in [larger, with_level] {
1422            let inbox = Arc::new(Inbox::default());
1423            paper.modify(exit.id.clone(), changed, inbox.reply(0));
1424            assert!(
1425                inbox
1426                    .drain()
1427                    .answers
1428                    .pop()
1429                    .unwrap()
1430                    .1
1431                    .downcast::<Result<(), FeedError>>()
1432                    .unwrap()
1433                    .is_err()
1434            );
1435            assert_eq!(snapshot(&paper), before);
1436        }
1437    }
1438
1439    #[test]
1440    fn only_gtc_orders_are_offered_and_accepted_without_session_boundaries() {
1441        let paper = PaperAccount::new("USD", 1_000.0);
1442        paper.set_price("SYM", 100.0);
1443        let inbox = Arc::new(Inbox::default());
1444        paper.rules("SYM", inbox.reply(0));
1445        let rules = *inbox
1446            .drain()
1447            .answers
1448            .pop()
1449            .unwrap()
1450            .1
1451            .downcast::<Result<TradingRules, FeedError>>()
1452            .unwrap();
1453        assert_eq!(
1454            rules.unwrap().time_in_force,
1455            vec![TimeInForce::GoodTillCancelled]
1456        );
1457        let mut request = OrderRequest::new("SYM", OrderSide::Buy, 1.0, OrderType::Limit(90.0));
1458        request.time_in_force = TimeInForce::GoodTillCancelled;
1459        let id = place(&paper, request.clone()).unwrap();
1460        let before = snapshot(&paper);
1461        for time_in_force in [
1462            TimeInForce::Day,
1463            TimeInForce::Week,
1464            TimeInForce::Month,
1465            TimeInForce::ImmediateOrCancel,
1466            TimeInForce::FillOrKill,
1467        ] {
1468            request.time_in_force = time_in_force;
1469            paper.place(request.clone(), inbox.reply(0));
1470            let result = *inbox
1471                .drain()
1472                .answers
1473                .pop()
1474                .unwrap()
1475                .1
1476                .downcast::<Result<OrderId, FeedError>>()
1477                .unwrap();
1478            assert_eq!(result, Err(FeedError::NotSupported));
1479            paper.modify(id.clone(), request.clone(), inbox.reply(0));
1480            let result = *inbox
1481                .drain()
1482                .answers
1483                .pop()
1484                .unwrap()
1485                .1
1486                .downcast::<Result<(), FeedError>>()
1487                .unwrap();
1488            assert_eq!(result, Err(FeedError::NotSupported));
1489            assert_eq!(snapshot(&paper), before);
1490        }
1491    }
1492
1493    #[test]
1494    fn take_profit_only_exits_reduce_the_position_and_fill_at_the_better_price() {
1495        for attached in [false, true] {
1496            for closed in [1.0, 2.0, 4.0] {
1497                let paper = PaperAccount::new("USD", 10_000.0);
1498                paper.set_price("SYM", 100.0);
1499                let mut entry = OrderRequest::new("SYM", OrderSide::Buy, 2.0, OrderType::Market);
1500                if attached {
1501                    entry.take_profit = Some(105.0);
1502                }
1503                place(&paper, entry).unwrap();
1504                if !attached {
1505                    let mut exit =
1506                        OrderRequest::new("SYM", OrderSide::Sell, 2.0, OrderType::Limit(105.0));
1507                    exit.position_bracket = Some(PositionBracket::TakeProfit);
1508                    place(&paper, exit).unwrap();
1509                }
1510                place(
1511                    &paper,
1512                    OrderRequest::new("SYM", OrderSide::Sell, closed, OrderType::Market),
1513                )
1514                .unwrap();
1515                let events = snapshot(&paper);
1516                let quantities: Vec<_> = events
1517                    .iter()
1518                    .filter_map(|event| match event {
1519                        AccountEvent::Order(order) if order.status == OrderStatus::Working => {
1520                            Some(order.request.quantity)
1521                        }
1522                        _ => None,
1523                    })
1524                    .collect();
1525                assert_eq!(
1526                    quantities,
1527                    if closed == 1.0 { vec![1.0] } else { vec![] },
1528                    "attached {attached}, closed {closed}"
1529                );
1530                paper.set_price("SYM", 106.0);
1531                paper.set_price("SYM", 94.0);
1532                let events = snapshot(&paper);
1533                let position = events
1534                    .iter()
1535                    .find_map(|event| match event {
1536                        AccountEvent::Position(position) => Some(position.quantity),
1537                        _ => None,
1538                    })
1539                    .unwrap_or(0.0);
1540                assert_eq!(position, if closed == 4.0 { -2.0 } else { 0.0 });
1541                assert!(!events.iter().any(
1542                    |e| matches!(e, AccountEvent::Order(o) if o.status == OrderStatus::Working)
1543                ));
1544                if closed == 1.0 {
1545                    // One unit bought at 100 and sold at the reaching price 106 earns 6, not 5.
1546                    let account = events
1547                        .iter()
1548                        .find_map(|event| match event {
1549                            AccountEvent::Account(account) => Some(account),
1550                            _ => None,
1551                        })
1552                        .unwrap();
1553                    assert_eq!(account.balance, 10_006.0);
1554                    let fill = events
1555                        .iter()
1556                        .find_map(|event| match event {
1557                            AccountEvent::Order(order)
1558                                if order.request.order_type == OrderType::Limit(105.0) =>
1559                            {
1560                                Some(order)
1561                            }
1562                            _ => None,
1563                        })
1564                        .unwrap();
1565                    assert_eq!(fill.fill_price, Some(106.0));
1566                    assert_eq!(fill.filled, 1.0);
1567                }
1568            }
1569        }
1570    }
1571}