1use 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
16pub 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 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
77fn 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 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 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 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 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 pub fn set_replay_time(&self, time: Option<DateTime<Utc>>) {
154 self.lock().replay_time = time;
155 }
156
157 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 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 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 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 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 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 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 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
605fn 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 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 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 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 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 #[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 for (held, entered, first, partly, second, last, position, balance) in [
1070 (
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 (
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 (
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 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 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 assert_eq!(state.balance, 999.1);
1291 assert_eq!(state.positions["A"].margin, Some(900.0));
1292 drop(state);
1293
1294 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 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 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 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 (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 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 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}