1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4use rand::distributions::Distribution;
5use rand_distr::Normal;
6use rand::thread_rng;
7use chrono::{DateTime, FixedOffset, Utc};
8use log::{info, warn, error};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use uuid::Uuid;
12
13use crate::trading_mode::SlippageConfig;
14use crate::unified_data::{
15 Position, OrderRequest, OrderResult, MarketData,
16 OrderSide, OrderType, OrderStatus,
17 TradingStrategy
18};
19use crate::real_time_data_stream::RealTimeDataStream;
20
21#[derive(Debug, Error)]
23pub enum PaperTradingError {
24 #[error("Market data not available for {0}")]
26 MarketDataNotAvailable(String),
27
28 #[error("Order execution failed: {0}")]
30 OrderExecutionFailed(String),
31
32 #[error("Position not found for {0}")]
34 PositionNotFound(String),
35
36 #[error("Insufficient balance: required {required}, available {available}")]
38 InsufficientBalance {
39 required: f64,
40 available: f64,
41 },
42
43 #[error("Real-time data stream error: {0}")]
45 RealTimeDataError(String),
46
47 #[error("Strategy execution error: {0}")]
49 StrategyError(String),
50}
51
52#[derive(Debug, Clone)]
54pub struct SimulatedOrder {
55 pub order_id: String,
57
58 pub request: OrderRequest,
60
61 pub result: OrderResult,
63
64 pub created_at: DateTime<FixedOffset>,
66
67 pub updated_at: DateTime<FixedOffset>,
69
70 pub execution_delay_ms: u64,
72
73 pub slippage_pct: f64,
75}
76
77pub struct PaperTradingEngine {
79 simulated_balance: f64,
81
82 simulated_positions: HashMap<String, Position>,
84
85 order_history: Vec<SimulatedOrder>,
87
88 active_orders: HashMap<String, SimulatedOrder>,
90
91 real_time_data: Option<Arc<Mutex<RealTimeDataStream>>>,
93
94 market_data_cache: HashMap<String, MarketData>,
96
97 slippage_config: SlippageConfig,
99
100 maker_fee: f64,
102 taker_fee: f64,
103
104 metrics: PaperTradingMetrics,
106
107 trade_log: Vec<TradeLogEntry>,
109
110 is_running: bool,
112
113 last_update: DateTime<FixedOffset>,
115}
116
117#[derive(Debug, Clone)]
119pub struct PaperTradingMetrics {
120 pub initial_balance: f64,
122
123 pub current_balance: f64,
125
126 pub realized_pnl: f64,
128
129 pub unrealized_pnl: f64,
131
132 pub funding_pnl: f64,
134
135 pub total_fees: f64,
137
138 pub trade_count: usize,
140
141 pub winning_trades: usize,
143
144 pub losing_trades: usize,
146
147 pub max_drawdown: f64,
149
150 pub max_drawdown_pct: f64,
152
153 pub peak_balance: f64,
155
156 pub start_time: DateTime<FixedOffset>,
158
159 pub last_update: DateTime<FixedOffset>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct TradeLogEntry {
166 pub id: String,
168
169 pub symbol: String,
171
172 pub side: OrderSide,
174
175 pub quantity: f64,
177
178 pub price: f64,
180
181 pub timestamp: DateTime<FixedOffset>,
183
184 pub fees: f64,
186
187 pub order_type: OrderType,
189
190 pub order_id: String,
192
193 pub pnl: Option<f64>,
195
196 pub metadata: HashMap<String, String>,
198}
199
200impl PaperTradingEngine {
201 pub fn new(initial_balance: f64, slippage_config: SlippageConfig) -> Self {
203 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
204
205 let metrics = PaperTradingMetrics {
206 initial_balance,
207 current_balance: initial_balance,
208 realized_pnl: 0.0,
209 unrealized_pnl: 0.0,
210 funding_pnl: 0.0,
211 total_fees: 0.0,
212 trade_count: 0,
213 winning_trades: 0,
214 losing_trades: 0,
215 max_drawdown: 0.0,
216 max_drawdown_pct: 0.0,
217 peak_balance: initial_balance,
218 start_time: now,
219 last_update: now,
220 };
221
222 Self {
223 simulated_balance: initial_balance,
224 simulated_positions: HashMap::new(),
225 order_history: Vec::new(),
226 active_orders: HashMap::new(),
227 real_time_data: None,
228 market_data_cache: HashMap::new(),
229 slippage_config,
230 maker_fee: 0.0002, taker_fee: 0.0005, metrics,
233 trade_log: Vec::new(),
234 is_running: false,
235 last_update: now,
236 }
237 }
238
239 pub fn set_real_time_data(&mut self, data_stream: Arc<Mutex<RealTimeDataStream>>) {
241 self.real_time_data = Some(data_stream);
242 }
243
244 pub fn set_fees(&mut self, maker_fee: f64, taker_fee: f64) {
246 self.maker_fee = maker_fee;
247 self.taker_fee = taker_fee;
248 }
249
250 pub fn get_balance(&self) -> f64 {
252 self.simulated_balance
253 }
254
255 pub fn get_positions(&self) -> &HashMap<String, Position> {
257 &self.simulated_positions
258 }
259
260 pub fn get_order_history(&self) -> &Vec<SimulatedOrder> {
262 &self.order_history
263 }
264
265 pub fn get_active_orders(&self) -> &HashMap<String, SimulatedOrder> {
267 &self.active_orders
268 }
269
270 pub fn get_trade_log(&self) -> &Vec<TradeLogEntry> {
272 &self.trade_log
273 }
274
275 pub fn get_metrics(&self) -> &PaperTradingMetrics {
277 &self.metrics
278 }
279
280 pub fn get_portfolio_value(&self) -> f64 {
282 let position_value = self.simulated_positions.values()
283 .map(|p| p.notional_value())
284 .sum::<f64>();
285
286 self.simulated_balance + position_value
287 }
288
289 pub fn update_market_data(&mut self, data: MarketData) -> Result<(), PaperTradingError> {
291 self.market_data_cache.insert(data.symbol.clone(), data.clone());
293
294 if let Some(position) = self.simulated_positions.get_mut(&data.symbol) {
296 position.update_price(data.price);
297 }
298
299 self.process_active_orders(&data)?;
301
302 self.update_metrics();
304
305 Ok(())
306 }
307
308 pub async fn execute_order(&mut self, order: OrderRequest) -> Result<OrderResult, PaperTradingError> {
310 if let Err(err) = order.validate() {
312 return Err(PaperTradingError::OrderExecutionFailed(err));
313 }
314
315 let market_data = self.get_market_data(&order.symbol)?;
317
318 let order_id = Uuid::new_v4().to_string();
320
321 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
323 let mut order_result = OrderResult::new(
324 &order_id,
325 &order.symbol,
326 order.side,
327 order.order_type,
328 order.quantity,
329 now,
330 );
331 order_result.status = OrderStatus::Submitted;
332
333 if order.order_type == OrderType::Market {
335 let execution_price = self.calculate_execution_price(&order, &market_data);
337
338 let fee_rate = self.taker_fee; let fee_amount = order.quantity * execution_price * fee_rate;
341
342 order_result.status = OrderStatus::Filled;
344 order_result.filled_quantity = order.quantity;
345 order_result.average_price = Some(execution_price);
346 order_result.fees = Some(fee_amount);
347
348 self.update_position_and_balance(&order, execution_price, fee_amount)?;
350
351 let simulated_order = SimulatedOrder {
353 order_id: order_id.clone(),
354 request: order.clone(),
355 result: order_result.clone(),
356 created_at: now,
357 updated_at: now,
358 execution_delay_ms: self.slippage_config.simulated_latency_ms,
359 slippage_pct: (execution_price - market_data.price) / market_data.price * 100.0,
360 };
361 self.order_history.push(simulated_order);
362
363 self.add_trade_log_entry(&order, &order_result);
365
366 self.update_metrics();
368
369 return Ok(order_result);
370 } else {
371 let simulated_order = SimulatedOrder {
373 order_id: order_id.clone(),
374 request: order.clone(),
375 result: order_result.clone(),
376 created_at: now,
377 updated_at: now,
378 execution_delay_ms: self.slippage_config.simulated_latency_ms,
379 slippage_pct: 0.0, };
381
382 self.active_orders.insert(order_id.clone(), simulated_order);
383
384 return Ok(order_result);
385 }
386 }
387
388 pub fn cancel_order(&mut self, order_id: &str) -> Result<OrderResult, PaperTradingError> {
390 if let Some(mut order) = self.active_orders.remove(order_id) {
392 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
394 order.result.status = OrderStatus::Cancelled;
395 order.updated_at = now;
396
397 self.order_history.push(order.clone());
399
400 return Ok(order.result);
401 } else {
402 return Err(PaperTradingError::OrderExecutionFailed(
403 format!("Order not found: {}", order_id)
404 ));
405 }
406 }
407
408 pub async fn start_simulation(&mut self, strategy: Box<dyn TradingStrategy>) -> Result<(), PaperTradingError> {
410 if self.real_time_data.is_none() {
411 return Err(PaperTradingError::RealTimeDataError(
412 "Real-time data stream not set".to_string()
413 ));
414 }
415
416 self.is_running = true;
417 info!("Starting paper trading simulation with strategy: {}", strategy.name());
418
419 while self.is_running {
421 self.process_market_data_updates(strategy.as_ref()).await?;
423
424 tokio::time::sleep(Duration::from_millis(100)).await;
426 }
427
428 info!("Paper trading simulation stopped");
429 Ok(())
430 }
431
432 pub fn stop_simulation(&mut self) {
434 self.is_running = false;
435 info!("Stopping paper trading simulation");
436 }
437
438 pub fn apply_funding_payment(&mut self, symbol: &str, payment: f64) -> Result<(), PaperTradingError> {
440 if let Some(position) = self.simulated_positions.get_mut(symbol) {
441 position.apply_funding_payment(payment);
442 self.metrics.funding_pnl += payment;
443 Ok(())
444 } else {
445 Err(PaperTradingError::PositionNotFound(symbol.to_string()))
446 }
447 }
448
449 fn get_market_data(&self, symbol: &str) -> Result<MarketData, PaperTradingError> {
451 if let Some(data) = self.market_data_cache.get(symbol) {
452 Ok(data.clone())
453 } else {
454 Err(PaperTradingError::MarketDataNotAvailable(symbol.to_string()))
455 }
456 }
457
458 fn calculate_execution_price(&self, order: &OrderRequest, market_data: &MarketData) -> f64 {
460 let base_price = match order.side {
461 OrderSide::Buy => market_data.ask, OrderSide::Sell => market_data.bid, };
464
465 let mut slippage_pct = self.slippage_config.base_slippage_pct;
467
468 let volume_impact = order.quantity / market_data.volume * self.slippage_config.volume_impact_factor;
470 slippage_pct += volume_impact;
471
472 let mut rng = thread_rng();
474 let normal = Normal::new(0.0, self.slippage_config.random_slippage_max_pct / 2.0).unwrap();
475 let random_slippage = normal.sample(&mut rng);
476 slippage_pct += random_slippage;
477
478 slippage_pct = slippage_pct.min(0.01); let slippage_factor = match order.side {
483 OrderSide::Buy => 1.0 + slippage_pct, OrderSide::Sell => 1.0 - slippage_pct, };
486
487 base_price * slippage_factor
488 }
489
490 fn update_position_and_balance(&mut self, order: &OrderRequest, execution_price: f64, fee_amount: f64) -> Result<(), PaperTradingError> {
492 let symbol = &order.symbol;
493 let quantity = order.quantity;
494 let side = order.side;
495
496 let order_cost = quantity * execution_price;
498
499 if side == OrderSide::Buy && order_cost + fee_amount > self.simulated_balance {
501 return Err(PaperTradingError::InsufficientBalance {
502 required: order_cost + fee_amount,
503 available: self.simulated_balance,
504 });
505 }
506
507 let position = self.simulated_positions.entry(symbol.clone())
509 .or_insert_with(|| {
510 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
511 Position::new(symbol, 0.0, 0.0, execution_price, now)
512 });
513
514 let position_change = match side {
516 OrderSide::Buy => quantity,
517 OrderSide::Sell => -quantity,
518 };
519
520 let mut realized_pnl = 0.0;
522 if (position.size > 0.0 && position_change < 0.0) || (position.size < 0.0 && position_change > 0.0) {
523 let closing_size = position_change.abs().min(position.size.abs());
525 realized_pnl = closing_size * (execution_price - position.entry_price) * position.size.signum();
526
527 self.metrics.realized_pnl += realized_pnl;
529 self.metrics.trade_count += 1;
530 if realized_pnl > 0.0 {
531 self.metrics.winning_trades += 1;
532 } else if realized_pnl < 0.0 {
533 self.metrics.losing_trades += 1;
534 }
535 }
536
537 if position.size + position_change == 0.0 {
539 position.size = 0.0;
541 position.entry_price = 0.0;
542 } else if position.size * (position.size + position_change) < 0.0 {
543 position.size = position_change;
545 position.entry_price = execution_price;
546 } else if position.size == 0.0 {
547 position.size = position_change;
549 position.entry_price = execution_price;
550 } else {
551 let old_notional = position.size.abs() * position.entry_price;
553 let new_notional = position_change.abs() * execution_price;
554 let total_size = position.size + position_change;
555
556 if total_size != 0.0 {
557 position.entry_price = (old_notional + new_notional) / total_size.abs();
558 }
559 position.size = total_size;
560 }
561
562 position.current_price = execution_price;
564
565 match side {
567 OrderSide::Buy => {
568 self.simulated_balance -= order_cost + fee_amount;
569 },
570 OrderSide::Sell => {
571 self.simulated_balance += order_cost - fee_amount;
572 self.simulated_balance += realized_pnl;
573 },
574 }
575
576 self.metrics.total_fees += fee_amount;
578 self.update_metrics();
579
580 Ok(())
581 }
582
583 fn process_active_orders(&mut self, market_data: &MarketData) -> Result<(), PaperTradingError> {
585 let symbol = &market_data.symbol;
586 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
587
588 let orders_to_process: Vec<String> = self.active_orders.iter()
590 .filter(|(_, order)| order.request.symbol == *symbol)
591 .map(|(id, _)| id.clone())
592 .collect();
593
594 for order_id in orders_to_process {
595 if let Some(mut order) = self.active_orders.remove(&order_id) {
596 let request = &order.request;
597
598 let should_execute = match (request.order_type, request.side) {
600 (OrderType::Limit, OrderSide::Buy) => {
601 if let Some(limit_price) = request.price {
603 market_data.ask <= limit_price
604 } else {
605 false
606 }
607 },
608 (OrderType::Limit, OrderSide::Sell) => {
609 if let Some(limit_price) = request.price {
611 market_data.bid >= limit_price
612 } else {
613 false
614 }
615 },
616 (OrderType::StopMarket, OrderSide::Buy) => {
617 if let Some(stop_price) = request.stop_price {
619 market_data.price >= stop_price
620 } else {
621 false
622 }
623 },
624 (OrderType::StopMarket, OrderSide::Sell) => {
625 if let Some(stop_price) = request.stop_price {
627 market_data.price <= stop_price
628 } else {
629 false
630 }
631 },
632 _ => false, };
634
635 if should_execute {
636 let execution_price = match request.order_type {
638 OrderType::Limit => request.price.unwrap_or(market_data.price),
639 _ => self.calculate_execution_price(request, market_data),
640 };
641
642 let fee_rate = match request.order_type {
644 OrderType::Limit => self.maker_fee, _ => self.taker_fee, };
647 let fee_amount = request.quantity * execution_price * fee_rate;
648
649 order.result.status = OrderStatus::Filled;
651 order.result.filled_quantity = request.quantity;
652 order.result.average_price = Some(execution_price);
653 order.result.fees = Some(fee_amount);
654 order.updated_at = now;
655
656 if let Err(err) = self.update_position_and_balance(request, execution_price, fee_amount) {
658 order.result.status = OrderStatus::Rejected;
660 order.result.error = Some(err.to_string());
661 }
662
663 self.order_history.push(order.clone());
665
666 if order.result.status == OrderStatus::Filled {
668 self.add_trade_log_entry(request, &order.result);
669 }
670 } else {
671 self.active_orders.insert(order_id, order);
673 }
674 }
675 }
676
677 Ok(())
678 }
679
680 async fn process_market_data_updates(&mut self, strategy: &dyn TradingStrategy) -> Result<(), PaperTradingError> {
682 if let Some(data_stream) = &self.real_time_data {
684 if let Ok(stream) = data_stream.lock() {
685 }
689 }
690
691 let market_data_vec: Vec<_> = self.market_data_cache.values().cloned().collect();
693
694 for market_data in market_data_vec {
696 match Ok(vec![]) as Result<Vec<OrderRequest>, String> {
699 Ok(order_requests) => {
700 for order_request in order_requests {
702 match self.execute_order(order_request).await {
703 Ok(_) => {},
704 Err(err) => {
705 warn!("Failed to execute order: {}", err);
706 }
707 }
708 }
709 },
710 Err(err) => {
711 return Err(PaperTradingError::StrategyError(err));
712 }
713 }
714 }
715
716 Ok(())
717 }
718
719 fn add_trade_log_entry(&mut self, order: &OrderRequest, result: &OrderResult) {
721 if result.status != OrderStatus::Filled || result.average_price.is_none() {
722 return;
723 }
724
725 let entry = TradeLogEntry {
726 id: Uuid::new_v4().to_string(),
727 symbol: order.symbol.clone(),
728 side: order.side,
729 quantity: result.filled_quantity,
730 price: result.average_price.unwrap(),
731 timestamp: result.timestamp,
732 fees: result.fees.unwrap_or(0.0),
733 order_type: order.order_type,
734 order_id: result.order_id.clone(),
735 pnl: None, metadata: HashMap::new(),
737 };
738
739 self.trade_log.push(entry);
740 }
741
742 fn update_metrics(&mut self) {
744 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
745
746 let unrealized_pnl = self.simulated_positions.values()
748 .map(|p| p.unrealized_pnl)
749 .sum::<f64>();
750
751 let funding_pnl = self.simulated_positions.values()
753 .map(|p| p.funding_pnl)
754 .sum::<f64>();
755
756 self.metrics.current_balance = self.simulated_balance;
758 self.metrics.unrealized_pnl = unrealized_pnl;
759 self.metrics.funding_pnl = funding_pnl;
760 self.metrics.last_update = now;
761
762 let total_equity = self.simulated_balance + unrealized_pnl + funding_pnl;
764 if total_equity > self.metrics.peak_balance {
765 self.metrics.peak_balance = total_equity;
766 } else {
767 let drawdown = self.metrics.peak_balance - total_equity;
768 let drawdown_pct = if self.metrics.peak_balance > 0.0 {
769 drawdown / self.metrics.peak_balance * 100.0
770 } else {
771 0.0
772 };
773
774 if drawdown > self.metrics.max_drawdown {
775 self.metrics.max_drawdown = drawdown;
776 self.metrics.max_drawdown_pct = drawdown_pct;
777 }
778 }
779 }
780
781 pub fn generate_report(&self) -> PaperTradingReport {
783 let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
784 let duration = now.signed_duration_since(self.metrics.start_time);
785 let duration_days = duration.num_milliseconds() as f64 / (1000.0 * 60.0 * 60.0 * 24.0);
786
787 let total_equity = self.simulated_balance + self.metrics.unrealized_pnl + self.metrics.funding_pnl;
788 let total_return = total_equity - self.metrics.initial_balance;
789 let total_return_pct = if self.metrics.initial_balance > 0.0 {
790 total_return / self.metrics.initial_balance * 100.0
791 } else {
792 0.0
793 };
794
795 let annualized_return = if duration_days > 0.0 {
796 (total_return_pct / 100.0 + 1.0).powf(365.0 / duration_days) - 1.0
797 } else {
798 0.0
799 } * 100.0;
800
801 let win_rate = if self.metrics.trade_count > 0 {
802 self.metrics.winning_trades as f64 / self.metrics.trade_count as f64 * 100.0
803 } else {
804 0.0
805 };
806
807 PaperTradingReport {
808 initial_balance: self.metrics.initial_balance,
809 current_balance: self.simulated_balance,
810 unrealized_pnl: self.metrics.unrealized_pnl,
811 realized_pnl: self.metrics.realized_pnl,
812 funding_pnl: self.metrics.funding_pnl,
813 total_pnl: self.metrics.realized_pnl + self.metrics.unrealized_pnl + self.metrics.funding_pnl,
814 total_fees: self.metrics.total_fees,
815 total_equity,
816 total_return,
817 total_return_pct,
818 annualized_return,
819 trade_count: self.metrics.trade_count,
820 winning_trades: self.metrics.winning_trades,
821 losing_trades: self.metrics.losing_trades,
822 win_rate,
823 max_drawdown: self.metrics.max_drawdown,
824 max_drawdown_pct: self.metrics.max_drawdown_pct,
825 start_time: self.metrics.start_time,
826 end_time: now,
827 duration_days,
828 }
829 }
830}
831
832#[derive(Debug, Clone)]
834pub struct PaperTradingReport {
835 pub initial_balance: f64,
837
838 pub current_balance: f64,
840
841 pub unrealized_pnl: f64,
843
844 pub realized_pnl: f64,
846
847 pub funding_pnl: f64,
849
850 pub total_pnl: f64,
852
853 pub total_fees: f64,
855
856 pub total_equity: f64,
858
859 pub total_return: f64,
861
862 pub total_return_pct: f64,
864
865 pub annualized_return: f64,
867
868 pub trade_count: usize,
870
871 pub winning_trades: usize,
873
874 pub losing_trades: usize,
876
877 pub win_rate: f64,
879
880 pub max_drawdown: f64,
882
883 pub max_drawdown_pct: f64,
885
886 pub start_time: DateTime<FixedOffset>,
888
889 pub end_time: DateTime<FixedOffset>,
891
892 pub duration_days: f64,
894}
895
896impl std::fmt::Display for PaperTradingReport {
897 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
898 writeln!(f, "=== Paper Trading Performance Report ===")?;
899 writeln!(f, "Period: {} to {}", self.start_time, self.end_time)?;
900 writeln!(f, "Duration: {:.2} days", self.duration_days)?;
901 writeln!(f, "")?;
902 writeln!(f, "Initial Balance: ${:.2}", self.initial_balance)?;
903 writeln!(f, "Current Balance: ${:.2}", self.current_balance)?;
904 writeln!(f, "Unrealized P&L: ${:.2}", self.unrealized_pnl)?;
905 writeln!(f, "Realized P&L: ${:.2}", self.realized_pnl)?;
906 writeln!(f, "Funding P&L: ${:.2}", self.funding_pnl)?;
907 writeln!(f, "Total P&L: ${:.2}", self.total_pnl)?;
908 writeln!(f, "Total Fees: ${:.2}", self.total_fees)?;
909 writeln!(f, "")?;
910 writeln!(f, "Total Equity: ${:.2}", self.total_equity)?;
911 writeln!(f, "Total Return: ${:.2} ({:.2}%)", self.total_return, self.total_return_pct)?;
912 writeln!(f, "Annualized Return: {:.2}%", self.annualized_return)?;
913 writeln!(f, "")?;
914 writeln!(f, "Trade Count: {}", self.trade_count)?;
915 writeln!(f, "Winning Trades: {} ({:.2}%)", self.winning_trades, self.win_rate)?;
916 writeln!(f, "Losing Trades: {}", self.losing_trades)?;
917 writeln!(f, "Maximum Drawdown: ${:.2} ({:.2}%)", self.max_drawdown, self.max_drawdown_pct)?;
918
919 Ok(())
920 }
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926 use std::sync::Arc;
927
928 struct MockStrategy {
930 name: String,
931 signals: HashMap<String, Signal>,
932 }
933
934 impl TradingStrategy for MockStrategy {
935 fn name(&self) -> &str {
936 &self.name
937 }
938
939 fn on_market_data(&mut self, data: &MarketData) -> Result<Vec<OrderRequest>, String> {
940 let symbol = &data.symbol;
942 let signal = self.signals.get(symbol);
943
944 match signal {
945 Some(signal) => {
946 match signal.direction {
947 SignalDirection::Buy => {
948 Ok(vec![OrderRequest::market(symbol, OrderSide::Buy, 1.0)])
949 },
950 SignalDirection::Sell => {
951 Ok(vec![OrderRequest::market(symbol, OrderSide::Sell, 1.0)])
952 },
953 _ => Ok(vec![]),
954 }
955 },
956 None => Ok(vec![]),
957 }
958 }
959
960 fn on_order_fill(&mut self, _fill: &OrderResult) -> Result<(), String> {
961 Ok(())
962 }
963
964 fn on_funding_payment(&mut self, _payment: &FundingPayment) -> Result<(), String> {
965 Ok(())
966 }
967
968 fn get_current_signals(&self) -> HashMap<String, Signal> {
969 self.signals.clone()
970 }
971 }
972
973 #[test]
974 fn test_paper_trading_engine_creation() {
975 let slippage_config = SlippageConfig::default();
976 let engine = PaperTradingEngine::new(10000.0, slippage_config);
977
978 assert_eq!(engine.simulated_balance, 10000.0);
979 assert!(engine.simulated_positions.is_empty());
980 assert!(engine.order_history.is_empty());
981 assert!(engine.active_orders.is_empty());
982 }
983
984 #[test]
985 fn test_market_data_update() {
986 let slippage_config = SlippageConfig::default();
987 let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
988
989 let now = Utc::now().with_timezone(&FixedOffset::east(0));
990 let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
991
992 let position = Position::new("BTC", 1.0, 49000.0, 49000.0, now);
994 engine.simulated_positions.insert("BTC".to_string(), position);
995
996 engine.update_market_data(market_data).unwrap();
998
999 let updated_position = engine.simulated_positions.get("BTC").unwrap();
1001 assert_eq!(updated_position.current_price, 50000.0);
1002 assert_eq!(updated_position.unrealized_pnl, 1000.0); }
1004
1005 #[test]
1006 fn test_market_order_execution() {
1007 let slippage_config = SlippageConfig {
1008 base_slippage_pct: 0.0, volume_impact_factor: 0.0,
1010 volatility_impact_factor: 0.0,
1011 random_slippage_max_pct: 0.0,
1012 simulated_latency_ms: 0,
1013 use_order_book: false,
1014 max_slippage_pct: 0.0,
1015 };
1016
1017 let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
1018
1019 let now = Utc::now().with_timezone(&FixedOffset::east(0));
1020 let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
1021
1022 engine.market_data_cache.insert("BTC".to_string(), market_data);
1024
1025 let order = OrderRequest::market("BTC", OrderSide::Buy, 0.1);
1027
1028 let rt = tokio::runtime::Runtime::new().unwrap();
1030 let result = rt.block_on(engine.execute_order(order)).unwrap();
1031
1032 assert_eq!(result.status, OrderStatus::Filled);
1034 assert_eq!(result.filled_quantity, 0.1);
1035 assert!(result.average_price.is_some());
1036 assert!(result.fees.is_some());
1037
1038 let position = engine.simulated_positions.get("BTC").unwrap();
1040 assert_eq!(position.size, 0.1);
1041 assert_eq!(position.entry_price, 50010.0); let fees = 0.1 * 50010.0 * engine.taker_fee;
1045 assert_eq!(engine.simulated_balance, 10000.0 - (0.1 * 50010.0) - fees);
1046 }
1047
1048 #[test]
1049 fn test_limit_order_execution() {
1050 let slippage_config = SlippageConfig::default();
1051 let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
1052
1053 let now = Utc::now().with_timezone(&FixedOffset::east(0));
1054 let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
1055
1056 engine.market_data_cache.insert("BTC".to_string(), market_data.clone());
1058
1059 let limit_price = 49980.0;
1061 let order = OrderRequest::limit("BTC", OrderSide::Buy, 0.1, limit_price);
1062
1063 let rt = tokio::runtime::Runtime::new().unwrap();
1065 let result = rt.block_on(engine.execute_order(order)).unwrap();
1066
1067 assert_eq!(result.status, OrderStatus::Submitted);
1069 assert_eq!(engine.active_orders.len(), 1);
1070
1071 let new_market_data = MarketData::new("BTC", 49970.0, 49960.0, 49980.0, 100.0, now);
1073 engine.update_market_data(new_market_data).unwrap();
1074
1075 assert_eq!(engine.active_orders.len(), 0);
1077 assert_eq!(engine.order_history.len(), 1);
1078
1079 let position = engine.simulated_positions.get("BTC").unwrap();
1081 assert_eq!(position.size, 0.1);
1082 assert_eq!(position.entry_price, limit_price);
1083 }
1084
1085 #[test]
1086 fn test_position_tracking() {
1087 let slippage_config = SlippageConfig {
1088 base_slippage_pct: 0.0, volume_impact_factor: 0.0,
1090 volatility_impact_factor: 0.0,
1091 random_slippage_max_pct: 0.0,
1092 simulated_latency_ms: 0,
1093 use_order_book: false,
1094 max_slippage_pct: 0.0,
1095 };
1096
1097 let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
1098
1099 let now = Utc::now().with_timezone(&FixedOffset::east(0));
1100 let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
1101
1102 engine.market_data_cache.insert("BTC".to_string(), market_data);
1104
1105 let buy_order = OrderRequest::market("BTC", OrderSide::Buy, 0.1);
1107 let rt = tokio::runtime::Runtime::new().unwrap();
1108 rt.block_on(engine.execute_order(buy_order)).unwrap();
1109
1110 let position = engine.simulated_positions.get("BTC").unwrap();
1112 assert_eq!(position.size, 0.1);
1113
1114 let sell_order = OrderRequest::market("BTC", OrderSide::Sell, 0.05);
1116 rt.block_on(engine.execute_order(sell_order)).unwrap();
1117
1118 let position = engine.simulated_positions.get("BTC").unwrap();
1120 assert_eq!(position.size, 0.05);
1121
1122 let sell_order = OrderRequest::market("BTC", OrderSide::Sell, 0.05);
1124 rt.block_on(engine.execute_order(sell_order)).unwrap();
1125
1126 let position = engine.simulated_positions.get("BTC").unwrap();
1128 assert_eq!(position.size, 0.0);
1129 }
1130
1131 #[test]
1132 fn test_performance_metrics() {
1133 let slippage_config = SlippageConfig {
1134 base_slippage_pct: 0.0, volume_impact_factor: 0.0,
1136 volatility_impact_factor: 0.0,
1137 random_slippage_max_pct: 0.0,
1138 simulated_latency_ms: 0,
1139 use_order_book: false,
1140 max_slippage_pct: 0.0,
1141 };
1142
1143 let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
1144
1145 let now = Utc::now().with_timezone(&FixedOffset::east(0));
1146
1147 let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
1149 engine.market_data_cache.insert("BTC".to_string(), market_data);
1150
1151 let buy_order = OrderRequest::market("BTC", OrderSide::Buy, 0.1);
1153 let rt = tokio::runtime::Runtime::new().unwrap();
1154 rt.block_on(engine.execute_order(buy_order)).unwrap();
1155
1156 let new_market_data = MarketData::new("BTC", 51000.0, 50990.0, 51010.0, 100.0, now);
1158 engine.update_market_data(new_market_data).unwrap();
1159
1160 let sell_order = OrderRequest::market("BTC", OrderSide::Sell, 0.1);
1162 rt.block_on(engine.execute_order(sell_order)).unwrap();
1163
1164 let metrics = engine.get_metrics();
1166 assert!(metrics.realized_pnl > 0.0); assert_eq!(metrics.trade_count, 1);
1168 assert_eq!(metrics.winning_trades, 1);
1169
1170 let report = engine.generate_report();
1172 assert!(report.total_return > 0.0);
1173 assert!(report.total_return_pct > 0.0);
1174 assert!(report.win_rate > 0.0);
1175 }
1176}