1use std::collections::{BTreeMap, HashMap, VecDeque};
36
37use rust_decimal::Decimal;
38use wickra_core::Candle;
39
40use crate::error::{Error, Result};
41use crate::events::{Event, OrderBookSnapshot};
42use crate::options::MarketType;
43use crate::symbol::Symbol;
44use crate::traits::{Exchange, Execution, MarketData};
45use crate::types::{Balance, Order, OrderRequest, OrderSide, OrderStatus, OrderType, Ticker};
46
47fn bps_fraction(bps: Decimal) -> Decimal {
49 bps / Decimal::from(10_000)
50}
51
52#[derive(Debug)]
55pub struct PaperExchange {
56 market_type: MarketType,
57 maker_bps: Decimal,
58 taker_bps: Decimal,
59 slippage_bps: Decimal,
60 marks: HashMap<Symbol, Decimal>,
62 balances: BTreeMap<String, Balance>,
64 orders: BTreeMap<String, Order>,
67 events: VecDeque<Event>,
69 next_id: u64,
70}
71
72impl Default for PaperExchange {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl PaperExchange {
79 #[must_use]
83 pub fn new() -> Self {
84 Self {
85 market_type: MarketType::Spot,
86 maker_bps: Decimal::ZERO,
87 taker_bps: Decimal::ZERO,
88 slippage_bps: Decimal::ZERO,
89 marks: HashMap::new(),
90 balances: BTreeMap::new(),
91 orders: BTreeMap::new(),
92 events: VecDeque::new(),
93 next_id: 0,
94 }
95 }
96
97 #[must_use]
99 pub fn with_market_type(mut self, market_type: MarketType) -> Self {
100 self.market_type = market_type;
101 self
102 }
103
104 #[must_use]
106 pub fn with_fees(mut self, maker_bps: Decimal, taker_bps: Decimal) -> Self {
107 self.maker_bps = maker_bps;
108 self.taker_bps = taker_bps;
109 self
110 }
111
112 #[must_use]
114 pub fn with_slippage_bps(mut self, slippage_bps: Decimal) -> Self {
115 self.slippage_bps = slippage_bps;
116 self
117 }
118
119 #[must_use]
121 pub fn with_balance(mut self, asset: impl Into<String>, amount: Decimal) -> Self {
122 let asset = asset.into();
123 let entry = self.balances.entry(asset.clone()).or_insert(Balance {
124 asset,
125 free: Decimal::ZERO,
126 locked: Decimal::ZERO,
127 });
128 entry.free += amount;
129 self
130 }
131
132 #[must_use]
134 pub fn market_type(&self) -> MarketType {
135 self.market_type
136 }
137
138 pub fn set_price(&mut self, symbol: &Symbol, price: Decimal) {
140 self.marks.insert(symbol.clone(), price);
141 }
142
143 pub fn cancel_all(&mut self) -> u32 {
147 let open: Vec<(Symbol, String)> = self
148 .orders
149 .values()
150 .filter(|order| order.is_open())
151 .map(|order| (order.symbol.clone(), order.id.clone()))
152 .collect();
153 let mut cancelled = 0;
154 for (symbol, id) in open {
155 if self.cancel_order(&symbol, &id).is_ok() {
156 cancelled += 1;
157 }
158 }
159 cancelled
160 }
161
162 #[must_use]
164 pub fn maker_bps(&self) -> Decimal {
165 self.maker_bps
166 }
167
168 #[must_use]
170 pub fn taker_bps(&self) -> Decimal {
171 self.taker_bps
172 }
173
174 fn next_order_id(&mut self) -> String {
175 self.next_id += 1;
176 format!("paper-{}", self.next_id)
177 }
178
179 fn balance_mut(&mut self, asset: &str) -> &mut Balance {
180 self.balances
181 .entry(asset.to_string())
182 .or_insert_with(|| Balance {
183 asset: asset.to_string(),
184 free: Decimal::ZERO,
185 locked: Decimal::ZERO,
186 })
187 }
188
189 fn free_of(&self, asset: &str) -> Decimal {
190 self.balances.get(asset).map_or(Decimal::ZERO, |b| b.free)
191 }
192
193 fn balance_snapshot(&self) -> Vec<Balance> {
194 self.balances.values().cloned().collect()
195 }
196
197 fn fill_price(&self, side: OrderSide, mark: Decimal) -> Decimal {
200 let slip = bps_fraction(self.slippage_bps);
201 match side {
202 OrderSide::Buy => mark * (Decimal::ONE + slip),
203 OrderSide::Sell => mark * (Decimal::ONE - slip),
204 }
205 }
206
207 fn settle_fill(&mut self, request: &OrderRequest, fill_price: Decimal) -> Order {
210 let base = request.symbol.base().to_string();
211 let quote = request.symbol.quote().to_string();
212 let qty = request.quantity;
213 let notional = qty * fill_price;
214 let fee = notional * bps_fraction(self.taker_bps);
215
216 match request.side {
217 OrderSide::Buy => {
218 self.balance_mut("e).free -= notional + fee;
219 self.balance_mut(&base).free += qty;
220 }
221 OrderSide::Sell => {
222 self.balance_mut(&base).free -= qty;
223 self.balance_mut("e).free += notional - fee;
224 }
225 }
226
227 let id = self.next_order_id();
228 let order = Order {
229 id: id.clone(),
230 client_order_id: request.client_order_id.clone(),
231 symbol: request.symbol.clone(),
232 side: request.side,
233 order_type: request.order_type,
234 status: OrderStatus::Filled,
235 quantity: qty,
236 filled_quantity: qty,
237 price: request.price,
238 average_price: Some(fill_price),
239 };
240 self.orders.insert(id, order.clone());
241 self.events.push_back(Event::OrderUpdate(order.clone()));
242 self.events
243 .push_back(Event::BalanceUpdate(self.balance_snapshot()));
244 order
245 }
246
247 fn rest_order(&mut self, request: &OrderRequest, price: Decimal) -> Order {
249 let (asset, amount) = match request.side {
250 OrderSide::Buy => (request.symbol.quote().to_string(), request.quantity * price),
251 OrderSide::Sell => (request.symbol.base().to_string(), request.quantity),
252 };
253 let balance = self.balance_mut(&asset);
254 balance.free -= amount;
255 balance.locked += amount;
256
257 let id = self.next_order_id();
258 let order = Order {
259 id: id.clone(),
260 client_order_id: request.client_order_id.clone(),
261 symbol: request.symbol.clone(),
262 side: request.side,
263 order_type: request.order_type,
264 status: OrderStatus::New,
265 quantity: request.quantity,
266 filled_quantity: Decimal::ZERO,
267 price: Some(price),
268 average_price: None,
269 };
270 self.orders.insert(id, order.clone());
271 self.events.push_back(Event::OrderUpdate(order.clone()));
272 order
273 }
274}
275
276impl MarketData for PaperExchange {
277 fn ticker(&mut self, symbol: &Symbol) -> Result<Ticker> {
278 let mark = self
279 .marks
280 .get(symbol)
281 .copied()
282 .ok_or_else(|| Error::NotFound(format!("no paper price set for {symbol}")))?;
283 Ok(Ticker {
284 symbol: symbol.clone(),
285 last: mark,
286 bid: mark,
287 ask: mark,
288 volume: Decimal::ZERO,
289 timestamp: 0,
292 })
293 }
294
295 fn klines(&mut self, _symbol: &Symbol, _interval: &str, _limit: u32) -> Result<Vec<Candle>> {
296 Err(Error::Exchange {
297 code: "unsupported".to_string(),
298 message: "paper exchange has no historical data; pair it with a live or replay feed"
299 .to_string(),
300 })
301 }
302
303 fn order_book(&mut self, _symbol: &Symbol, _depth: u32) -> Result<OrderBookSnapshot> {
304 Err(Error::Exchange {
305 code: "unsupported".to_string(),
306 message: "paper exchange has no depth feed; pair it with a live or replay feed"
307 .to_string(),
308 })
309 }
310
311 fn subscribe_trades(&mut self, _symbol: &Symbol) -> Result<()> {
312 Ok(())
313 }
314
315 fn subscribe_book(&mut self, _symbol: &Symbol) -> Result<()> {
316 Ok(())
317 }
318
319 fn subscribe_ticker(&mut self, _symbol: &Symbol) -> Result<()> {
320 Ok(())
321 }
322
323 fn poll_events(&mut self) -> Vec<Event> {
324 self.events.drain(..).collect()
325 }
326}
327
328impl Execution for PaperExchange {
329 fn place_order(&mut self, request: &OrderRequest) -> Result<Order> {
330 request.validate()?;
331 if request.order_type.is_trigger() {
332 return Err(Error::InvalidOrder(
333 "paper exchange supports only market and limit orders",
334 ));
335 }
336 let mark =
337 self.marks.get(&request.symbol).copied().ok_or_else(|| {
338 Error::NotFound(format!("no paper price set for {}", request.symbol))
339 })?;
340
341 let crosses = match request.order_type {
345 OrderType::Market | OrderType::StopMarket => true,
346 OrderType::Limit | OrderType::StopLimit => match request.side {
347 OrderSide::Buy => request.price.unwrap_or(mark) >= mark,
348 OrderSide::Sell => request.price.unwrap_or(mark) <= mark,
349 },
350 };
351
352 if !crosses {
353 let price = request.price.unwrap_or(mark);
355 let (asset, amount) = match request.side {
356 OrderSide::Buy => (request.symbol.quote(), request.quantity * price),
357 OrderSide::Sell => (request.symbol.base(), request.quantity),
358 };
359 if self.free_of(asset) < amount {
360 return Err(Error::InsufficientBalance);
361 }
362 return Ok(self.rest_order(request, price));
363 }
364
365 if request.post_only {
366 return Err(Error::OrderRejected {
367 code: "post_only".to_string(),
368 message: "post-only order would take liquidity".to_string(),
369 });
370 }
371
372 let fill_price = self.fill_price(request.side, mark);
373 let notional = request.quantity * fill_price;
374 let fee = notional * bps_fraction(self.taker_bps);
375
376 match request.side {
378 OrderSide::Buy => {
379 if self.free_of(request.symbol.quote()) < notional + fee {
380 return Err(Error::InsufficientBalance);
381 }
382 }
383 OrderSide::Sell => {
384 if self.free_of(request.symbol.base()) < request.quantity {
385 return Err(Error::InsufficientBalance);
386 }
387 }
388 }
389
390 Ok(self.settle_fill(request, fill_price))
393 }
394
395 fn cancel_order(&mut self, _symbol: &Symbol, order_id: &str) -> Result<()> {
396 let order = self
397 .orders
398 .get_mut(order_id)
399 .filter(|o| o.is_open())
400 .ok_or_else(|| Error::NotFound(format!("no open paper order {order_id}")))?;
401 order.status = OrderStatus::Canceled;
402 let order = order.clone();
403
404 let price = order.price.unwrap_or_default();
406 let (asset, amount) = match order.side {
407 OrderSide::Buy => (order.symbol.quote().to_string(), order.quantity * price),
408 OrderSide::Sell => (order.symbol.base().to_string(), order.quantity),
409 };
410 let balance = self.balance_mut(&asset);
411 balance.locked -= amount;
412 balance.free += amount;
413
414 self.events.push_back(Event::OrderUpdate(order));
415 self.events
416 .push_back(Event::BalanceUpdate(self.balance_snapshot()));
417 Ok(())
418 }
419
420 fn query_order(&mut self, _symbol: &Symbol, order_id: &str) -> Result<Order> {
421 self.orders
422 .get(order_id)
423 .cloned()
424 .ok_or_else(|| Error::NotFound(format!("no paper order {order_id}")))
425 }
426
427 fn open_orders(&mut self, symbol: Option<&Symbol>) -> Result<Vec<Order>> {
428 Ok(self
429 .orders
430 .values()
431 .filter(|o| o.is_open())
432 .filter(|o| symbol.is_none_or(|s| &o.symbol == s))
433 .cloned()
434 .collect())
435 }
436
437 fn balances(&mut self) -> Result<Vec<Balance>> {
438 Ok(self.balance_snapshot())
439 }
440}
441
442impl Exchange for PaperExchange {
443 fn name(&self) -> &'static str {
444 "paper"
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use rust_decimal_macros::dec;
452
453 fn sym() -> Symbol {
454 Symbol::new("BTC", "USDT")
455 }
456
457 fn seeded() -> PaperExchange {
458 let mut ex = PaperExchange::new()
459 .with_fees(dec!(1), dec!(5))
460 .with_slippage_bps(dec!(10))
461 .with_balance("USDT", dec!(100000))
462 .with_balance("BTC", dec!(2));
463 ex.set_price(&sym(), dec!(20000));
464 ex
465 }
466
467 #[test]
468 fn market_buy_fills_with_slippage_and_fee() {
469 let mut ex = seeded();
470 let order = ex
471 .place_order(&OrderRequest::market_buy(sym(), dec!(1)))
472 .unwrap();
473 assert_eq!(order.status, OrderStatus::Filled);
474 assert_eq!(order.average_price, Some(dec!(20020)));
476 assert_eq!(order.filled_quantity, dec!(1));
477
478 let usdt = ex
480 .balances()
481 .unwrap()
482 .into_iter()
483 .find(|b| b.asset == "USDT")
484 .unwrap();
485 assert_eq!(usdt.free, dec!(100000) - dec!(20020) - dec!(10.01));
486 let btc = ex
487 .balances()
488 .unwrap()
489 .into_iter()
490 .find(|b| b.asset == "BTC")
491 .unwrap();
492 assert_eq!(btc.free, dec!(3));
493 }
494
495 #[test]
496 fn market_sell_credits_quote_net_of_fee() {
497 let mut ex = seeded();
498 ex.place_order(&OrderRequest::market_sell(sym(), dec!(1)))
499 .unwrap();
500 let usdt = ex.free_of("USDT");
502 assert_eq!(usdt, dec!(100000) + dec!(19980) - dec!(9.99));
503 assert_eq!(ex.free_of("BTC"), dec!(1));
504 }
505
506 #[test]
507 fn frictionless_fill_matches_the_mark_exactly() {
508 let mut ex = PaperExchange::new().with_balance("USDT", dec!(50000));
509 ex.set_price(&sym(), dec!(20000));
510 let order = ex
511 .place_order(&OrderRequest::market_buy(sym(), dec!(1)))
512 .unwrap();
513 assert_eq!(order.average_price, Some(dec!(20000)));
514 assert_eq!(ex.free_of("USDT"), dec!(30000));
515 }
516
517 #[test]
518 fn poll_events_emits_order_then_balance_update() {
519 let mut ex = seeded();
520 ex.place_order(&OrderRequest::market_buy(sym(), dec!(1)))
521 .unwrap();
522 let events = ex.poll_events();
523 assert_eq!(events.len(), 2);
524 assert!(matches!(events[0], Event::OrderUpdate(_)));
525 assert!(matches!(events[1], Event::BalanceUpdate(_)));
526 assert_eq!(ex.poll_events(), Vec::new());
528 }
529
530 #[test]
531 fn resting_limit_locks_funds_and_is_cancelable() {
532 let mut ex = seeded();
533 let order = ex
535 .place_order(&OrderRequest::limit_buy(sym(), dec!(1), dec!(19000)))
536 .unwrap();
537 assert_eq!(order.status, OrderStatus::New);
538 assert_eq!(ex.free_of("USDT"), dec!(100000) - dec!(19000));
539 assert_eq!(ex.open_orders(None).unwrap().len(), 1);
540
541 ex.cancel_order(&sym(), &order.id).unwrap();
542 assert_eq!(ex.free_of("USDT"), dec!(100000));
543 assert_eq!(ex.open_orders(None).unwrap(), Vec::new());
544 assert_eq!(
545 ex.query_order(&sym(), &order.id).unwrap().status,
546 OrderStatus::Canceled
547 );
548 }
549
550 #[test]
551 fn dead_mans_switch_fires_cancel_all_on_lost_heartbeat() {
552 use crate::DeadMansSwitch;
553 use std::time::Duration;
554
555 let mut ex = seeded();
556 ex.place_order(&OrderRequest::limit_buy(sym(), dec!(1), dec!(19000)))
558 .unwrap();
559 ex.place_order(&OrderRequest::limit_sell(sym(), dec!(1), dec!(21000)))
560 .unwrap();
561 assert_eq!(ex.open_orders(None).unwrap().len(), 2);
562
563 let mut switch = DeadMansSwitch::new(Duration::from_secs(5));
564 switch.heartbeat(1_000);
565 assert!(!switch.is_expired(3_000)); assert!(switch.is_expired(6_000));
569 let cancelled = ex.cancel_all();
570 assert_eq!(cancelled, 2);
571 assert_eq!(ex.open_orders(None).unwrap(), Vec::new());
572 assert_eq!(ex.free_of("USDT"), dec!(100000));
574 assert_eq!(ex.free_of("BTC"), dec!(2));
575 }
576
577 #[test]
578 fn marketable_limit_buy_fills_immediately() {
579 let mut ex = seeded();
580 let order = ex
582 .place_order(&OrderRequest::limit_buy(sym(), dec!(1), dec!(21000)))
583 .unwrap();
584 assert_eq!(order.status, OrderStatus::Filled);
585 assert_eq!(order.average_price, Some(dec!(20020)));
586 }
587
588 #[test]
589 fn post_only_crossing_order_is_rejected() {
590 let mut ex = seeded();
591 let err = ex
592 .place_order(&OrderRequest::limit_buy(sym(), dec!(1), dec!(21000)).post_only())
593 .unwrap_err();
594 assert!(matches!(err, Error::OrderRejected { .. }));
595 }
596
597 #[test]
598 fn insufficient_balance_is_rejected() {
599 let mut ex = PaperExchange::new().with_balance("USDT", dec!(100));
600 ex.set_price(&sym(), dec!(20000));
601 let err = ex
602 .place_order(&OrderRequest::market_buy(sym(), dec!(1)))
603 .unwrap_err();
604 assert!(matches!(err, Error::InsufficientBalance));
605 }
606
607 #[test]
608 fn insufficient_base_rejects_a_sell() {
609 let mut ex = PaperExchange::new().with_balance("BTC", dec!(0.5));
610 ex.set_price(&sym(), dec!(20000));
611 let err = ex
612 .place_order(&OrderRequest::market_sell(sym(), dec!(1)))
613 .unwrap_err();
614 assert!(matches!(err, Error::InsufficientBalance));
615 }
616
617 #[test]
618 fn stop_orders_are_unsupported() {
619 let mut ex = seeded();
620 let request = OrderRequest {
621 order_type: OrderType::StopMarket,
622 stop_price: Some(dec!(19000)),
623 ..OrderRequest::market_sell(sym(), dec!(1))
624 };
625 assert!(matches!(
626 ex.place_order(&request).unwrap_err(),
627 Error::InvalidOrder(_)
628 ));
629 }
630
631 #[test]
632 fn order_without_a_price_needs_a_mark() {
633 let mut ex = PaperExchange::new().with_balance("USDT", dec!(50000));
634 let err = ex
635 .place_order(&OrderRequest::market_buy(sym(), dec!(1)))
636 .unwrap_err();
637 assert!(matches!(err, Error::NotFound(_)));
638 }
639
640 #[test]
641 fn ticker_reports_the_mark_and_klines_are_unsupported() {
642 let mut ex = seeded();
643 let ticker = ex.ticker(&sym()).unwrap();
644 assert_eq!(ticker.last, dec!(20000));
645 assert_eq!(ticker.bid, ticker.ask);
646 assert!(ex.klines(&sym(), "1m", 10).is_err());
647 assert!(ex.order_book(&sym(), 10).is_err());
648 assert!(ex.ticker(&Symbol::new("ETH", "USDT")).is_err());
649 }
650
651 #[test]
652 fn subscriptions_are_inert_and_name_is_paper() {
653 let mut ex = seeded();
654 assert!(ex.subscribe_trades(&sym()).is_ok());
655 assert!(ex.subscribe_book(&sym()).is_ok());
656 assert!(ex.subscribe_ticker(&sym()).is_ok());
657 assert_eq!(ex.poll_events(), Vec::new());
658 assert_eq!(ex.name(), "paper");
659 assert_eq!(ex.market_type(), MarketType::Spot);
660 }
661
662 #[test]
663 fn cancel_unknown_order_is_not_found() {
664 let mut ex = seeded();
665 assert!(matches!(
666 ex.cancel_order(&sym(), "paper-999").unwrap_err(),
667 Error::NotFound(_)
668 ));
669 assert!(matches!(
670 ex.query_order(&sym(), "paper-999").unwrap_err(),
671 Error::NotFound(_)
672 ));
673 }
674
675 #[test]
676 fn builder_configuration_is_readable() {
677 let ex = PaperExchange::default()
678 .with_market_type(MarketType::UsdMFutures)
679 .with_fees(dec!(2), dec!(7));
680 assert_eq!(ex.market_type(), MarketType::UsdMFutures);
681 assert_eq!(ex.maker_bps(), dec!(2));
682 assert_eq!(ex.taker_bps(), dec!(7));
683 }
684
685 #[test]
688 fn debug_renders() {
689 let rendered = format!("{:?}", seeded());
690 assert!(rendered.starts_with("PaperExchange {"));
691 }
692}