use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use rand::distributions::Distribution;
use rand_distr::Normal;
use rand::thread_rng;
use chrono::{DateTime, FixedOffset, Utc};
use log::{info, warn, error};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
use crate::trading_mode::SlippageConfig;
use crate::unified_data::{
Position, OrderRequest, OrderResult, MarketData,
OrderSide, OrderType, OrderStatus,
TradingStrategy
};
use crate::real_time_data_stream::RealTimeDataStream;
#[derive(Debug, Error)]
pub enum PaperTradingError {
#[error("Market data not available for {0}")]
MarketDataNotAvailable(String),
#[error("Order execution failed: {0}")]
OrderExecutionFailed(String),
#[error("Position not found for {0}")]
PositionNotFound(String),
#[error("Insufficient balance: required {required}, available {available}")]
InsufficientBalance {
required: f64,
available: f64,
},
#[error("Real-time data stream error: {0}")]
RealTimeDataError(String),
#[error("Strategy execution error: {0}")]
StrategyError(String),
}
#[derive(Debug, Clone)]
pub struct SimulatedOrder {
pub order_id: String,
pub request: OrderRequest,
pub result: OrderResult,
pub created_at: DateTime<FixedOffset>,
pub updated_at: DateTime<FixedOffset>,
pub execution_delay_ms: u64,
pub slippage_pct: f64,
}
pub struct PaperTradingEngine {
simulated_balance: f64,
simulated_positions: HashMap<String, Position>,
order_history: Vec<SimulatedOrder>,
active_orders: HashMap<String, SimulatedOrder>,
real_time_data: Option<Arc<Mutex<RealTimeDataStream>>>,
market_data_cache: HashMap<String, MarketData>,
slippage_config: SlippageConfig,
maker_fee: f64,
taker_fee: f64,
metrics: PaperTradingMetrics,
trade_log: Vec<TradeLogEntry>,
is_running: bool,
last_update: DateTime<FixedOffset>,
}
#[derive(Debug, Clone)]
pub struct PaperTradingMetrics {
pub initial_balance: f64,
pub current_balance: f64,
pub realized_pnl: f64,
pub unrealized_pnl: f64,
pub funding_pnl: f64,
pub total_fees: f64,
pub trade_count: usize,
pub winning_trades: usize,
pub losing_trades: usize,
pub max_drawdown: f64,
pub max_drawdown_pct: f64,
pub peak_balance: f64,
pub start_time: DateTime<FixedOffset>,
pub last_update: DateTime<FixedOffset>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeLogEntry {
pub id: String,
pub symbol: String,
pub side: OrderSide,
pub quantity: f64,
pub price: f64,
pub timestamp: DateTime<FixedOffset>,
pub fees: f64,
pub order_type: OrderType,
pub order_id: String,
pub pnl: Option<f64>,
pub metadata: HashMap<String, String>,
}
impl PaperTradingEngine {
pub fn new(initial_balance: f64, slippage_config: SlippageConfig) -> Self {
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let metrics = PaperTradingMetrics {
initial_balance,
current_balance: initial_balance,
realized_pnl: 0.0,
unrealized_pnl: 0.0,
funding_pnl: 0.0,
total_fees: 0.0,
trade_count: 0,
winning_trades: 0,
losing_trades: 0,
max_drawdown: 0.0,
max_drawdown_pct: 0.0,
peak_balance: initial_balance,
start_time: now,
last_update: now,
};
Self {
simulated_balance: initial_balance,
simulated_positions: HashMap::new(),
order_history: Vec::new(),
active_orders: HashMap::new(),
real_time_data: None,
market_data_cache: HashMap::new(),
slippage_config,
maker_fee: 0.0002, taker_fee: 0.0005, metrics,
trade_log: Vec::new(),
is_running: false,
last_update: now,
}
}
pub fn set_real_time_data(&mut self, data_stream: Arc<Mutex<RealTimeDataStream>>) {
self.real_time_data = Some(data_stream);
}
pub fn set_fees(&mut self, maker_fee: f64, taker_fee: f64) {
self.maker_fee = maker_fee;
self.taker_fee = taker_fee;
}
pub fn get_balance(&self) -> f64 {
self.simulated_balance
}
pub fn get_positions(&self) -> &HashMap<String, Position> {
&self.simulated_positions
}
pub fn get_order_history(&self) -> &Vec<SimulatedOrder> {
&self.order_history
}
pub fn get_active_orders(&self) -> &HashMap<String, SimulatedOrder> {
&self.active_orders
}
pub fn get_trade_log(&self) -> &Vec<TradeLogEntry> {
&self.trade_log
}
pub fn get_metrics(&self) -> &PaperTradingMetrics {
&self.metrics
}
pub fn get_portfolio_value(&self) -> f64 {
let position_value = self.simulated_positions.values()
.map(|p| p.notional_value())
.sum::<f64>();
self.simulated_balance + position_value
}
pub fn update_market_data(&mut self, data: MarketData) -> Result<(), PaperTradingError> {
self.market_data_cache.insert(data.symbol.clone(), data.clone());
if let Some(position) = self.simulated_positions.get_mut(&data.symbol) {
position.update_price(data.price);
}
self.process_active_orders(&data)?;
self.update_metrics();
Ok(())
}
pub async fn execute_order(&mut self, order: OrderRequest) -> Result<OrderResult, PaperTradingError> {
if let Err(err) = order.validate() {
return Err(PaperTradingError::OrderExecutionFailed(err));
}
let market_data = self.get_market_data(&order.symbol)?;
let order_id = Uuid::new_v4().to_string();
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let mut order_result = OrderResult::new(
&order_id,
&order.symbol,
order.side,
order.order_type,
order.quantity,
now,
);
order_result.status = OrderStatus::Submitted;
if order.order_type == OrderType::Market {
let execution_price = self.calculate_execution_price(&order, &market_data);
let fee_rate = self.taker_fee; let fee_amount = order.quantity * execution_price * fee_rate;
order_result.status = OrderStatus::Filled;
order_result.filled_quantity = order.quantity;
order_result.average_price = Some(execution_price);
order_result.fees = Some(fee_amount);
self.update_position_and_balance(&order, execution_price, fee_amount)?;
let simulated_order = SimulatedOrder {
order_id: order_id.clone(),
request: order.clone(),
result: order_result.clone(),
created_at: now,
updated_at: now,
execution_delay_ms: self.slippage_config.simulated_latency_ms,
slippage_pct: (execution_price - market_data.price) / market_data.price * 100.0,
};
self.order_history.push(simulated_order);
self.add_trade_log_entry(&order, &order_result);
self.update_metrics();
return Ok(order_result);
} else {
let simulated_order = SimulatedOrder {
order_id: order_id.clone(),
request: order.clone(),
result: order_result.clone(),
created_at: now,
updated_at: now,
execution_delay_ms: self.slippage_config.simulated_latency_ms,
slippage_pct: 0.0, };
self.active_orders.insert(order_id.clone(), simulated_order);
return Ok(order_result);
}
}
pub fn cancel_order(&mut self, order_id: &str) -> Result<OrderResult, PaperTradingError> {
if let Some(mut order) = self.active_orders.remove(order_id) {
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
order.result.status = OrderStatus::Cancelled;
order.updated_at = now;
self.order_history.push(order.clone());
return Ok(order.result);
} else {
return Err(PaperTradingError::OrderExecutionFailed(
format!("Order not found: {}", order_id)
));
}
}
pub async fn start_simulation(&mut self, strategy: Box<dyn TradingStrategy>) -> Result<(), PaperTradingError> {
if self.real_time_data.is_none() {
return Err(PaperTradingError::RealTimeDataError(
"Real-time data stream not set".to_string()
));
}
self.is_running = true;
info!("Starting paper trading simulation with strategy: {}", strategy.name());
while self.is_running {
self.process_market_data_updates(strategy.as_ref()).await?;
tokio::time::sleep(Duration::from_millis(100)).await;
}
info!("Paper trading simulation stopped");
Ok(())
}
pub fn stop_simulation(&mut self) {
self.is_running = false;
info!("Stopping paper trading simulation");
}
pub fn apply_funding_payment(&mut self, symbol: &str, payment: f64) -> Result<(), PaperTradingError> {
if let Some(position) = self.simulated_positions.get_mut(symbol) {
position.apply_funding_payment(payment);
self.metrics.funding_pnl += payment;
Ok(())
} else {
Err(PaperTradingError::PositionNotFound(symbol.to_string()))
}
}
fn get_market_data(&self, symbol: &str) -> Result<MarketData, PaperTradingError> {
if let Some(data) = self.market_data_cache.get(symbol) {
Ok(data.clone())
} else {
Err(PaperTradingError::MarketDataNotAvailable(symbol.to_string()))
}
}
fn calculate_execution_price(&self, order: &OrderRequest, market_data: &MarketData) -> f64 {
let base_price = match order.side {
OrderSide::Buy => market_data.ask, OrderSide::Sell => market_data.bid, };
let mut slippage_pct = self.slippage_config.base_slippage_pct;
let volume_impact = order.quantity / market_data.volume * self.slippage_config.volume_impact_factor;
slippage_pct += volume_impact;
let mut rng = thread_rng();
let normal = Normal::new(0.0, self.slippage_config.random_slippage_max_pct / 2.0).unwrap();
let random_slippage = normal.sample(&mut rng);
slippage_pct += random_slippage;
slippage_pct = slippage_pct.min(0.01);
let slippage_factor = match order.side {
OrderSide::Buy => 1.0 + slippage_pct, OrderSide::Sell => 1.0 - slippage_pct, };
base_price * slippage_factor
}
fn update_position_and_balance(&mut self, order: &OrderRequest, execution_price: f64, fee_amount: f64) -> Result<(), PaperTradingError> {
let symbol = &order.symbol;
let quantity = order.quantity;
let side = order.side;
let order_cost = quantity * execution_price;
if side == OrderSide::Buy && order_cost + fee_amount > self.simulated_balance {
return Err(PaperTradingError::InsufficientBalance {
required: order_cost + fee_amount,
available: self.simulated_balance,
});
}
let position = self.simulated_positions.entry(symbol.clone())
.or_insert_with(|| {
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
Position::new(symbol, 0.0, 0.0, execution_price, now)
});
let position_change = match side {
OrderSide::Buy => quantity,
OrderSide::Sell => -quantity,
};
let mut realized_pnl = 0.0;
if (position.size > 0.0 && position_change < 0.0) || (position.size < 0.0 && position_change > 0.0) {
let closing_size = position_change.abs().min(position.size.abs());
realized_pnl = closing_size * (execution_price - position.entry_price) * position.size.signum();
self.metrics.realized_pnl += realized_pnl;
self.metrics.trade_count += 1;
if realized_pnl > 0.0 {
self.metrics.winning_trades += 1;
} else if realized_pnl < 0.0 {
self.metrics.losing_trades += 1;
}
}
if position.size + position_change == 0.0 {
position.size = 0.0;
position.entry_price = 0.0;
} else if position.size * (position.size + position_change) < 0.0 {
position.size = position_change;
position.entry_price = execution_price;
} else if position.size == 0.0 {
position.size = position_change;
position.entry_price = execution_price;
} else {
let old_notional = position.size.abs() * position.entry_price;
let new_notional = position_change.abs() * execution_price;
let total_size = position.size + position_change;
if total_size != 0.0 {
position.entry_price = (old_notional + new_notional) / total_size.abs();
}
position.size = total_size;
}
position.current_price = execution_price;
match side {
OrderSide::Buy => {
self.simulated_balance -= order_cost + fee_amount;
},
OrderSide::Sell => {
self.simulated_balance += order_cost - fee_amount;
self.simulated_balance += realized_pnl;
},
}
self.metrics.total_fees += fee_amount;
self.update_metrics();
Ok(())
}
fn process_active_orders(&mut self, market_data: &MarketData) -> Result<(), PaperTradingError> {
let symbol = &market_data.symbol;
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let orders_to_process: Vec<String> = self.active_orders.iter()
.filter(|(_, order)| order.request.symbol == *symbol)
.map(|(id, _)| id.clone())
.collect();
for order_id in orders_to_process {
if let Some(mut order) = self.active_orders.remove(&order_id) {
let request = &order.request;
let should_execute = match (request.order_type, request.side) {
(OrderType::Limit, OrderSide::Buy) => {
if let Some(limit_price) = request.price {
market_data.ask <= limit_price
} else {
false
}
},
(OrderType::Limit, OrderSide::Sell) => {
if let Some(limit_price) = request.price {
market_data.bid >= limit_price
} else {
false
}
},
(OrderType::StopMarket, OrderSide::Buy) => {
if let Some(stop_price) = request.stop_price {
market_data.price >= stop_price
} else {
false
}
},
(OrderType::StopMarket, OrderSide::Sell) => {
if let Some(stop_price) = request.stop_price {
market_data.price <= stop_price
} else {
false
}
},
_ => false, };
if should_execute {
let execution_price = match request.order_type {
OrderType::Limit => request.price.unwrap_or(market_data.price),
_ => self.calculate_execution_price(request, market_data),
};
let fee_rate = match request.order_type {
OrderType::Limit => self.maker_fee, _ => self.taker_fee, };
let fee_amount = request.quantity * execution_price * fee_rate;
order.result.status = OrderStatus::Filled;
order.result.filled_quantity = request.quantity;
order.result.average_price = Some(execution_price);
order.result.fees = Some(fee_amount);
order.updated_at = now;
if let Err(err) = self.update_position_and_balance(request, execution_price, fee_amount) {
order.result.status = OrderStatus::Rejected;
order.result.error = Some(err.to_string());
}
self.order_history.push(order.clone());
if order.result.status == OrderStatus::Filled {
self.add_trade_log_entry(request, &order.result);
}
} else {
self.active_orders.insert(order_id, order);
}
}
}
Ok(())
}
async fn process_market_data_updates(&mut self, strategy: &dyn TradingStrategy) -> Result<(), PaperTradingError> {
if let Some(data_stream) = &self.real_time_data {
if let Ok(stream) = data_stream.lock() {
}
}
let market_data_vec: Vec<_> = self.market_data_cache.values().cloned().collect();
for market_data in market_data_vec {
match Ok(vec![]) as Result<Vec<OrderRequest>, String> {
Ok(order_requests) => {
for order_request in order_requests {
match self.execute_order(order_request).await {
Ok(_) => {},
Err(err) => {
warn!("Failed to execute order: {}", err);
}
}
}
},
Err(err) => {
return Err(PaperTradingError::StrategyError(err));
}
}
}
Ok(())
}
fn add_trade_log_entry(&mut self, order: &OrderRequest, result: &OrderResult) {
if result.status != OrderStatus::Filled || result.average_price.is_none() {
return;
}
let entry = TradeLogEntry {
id: Uuid::new_v4().to_string(),
symbol: order.symbol.clone(),
side: order.side,
quantity: result.filled_quantity,
price: result.average_price.unwrap(),
timestamp: result.timestamp,
fees: result.fees.unwrap_or(0.0),
order_type: order.order_type,
order_id: result.order_id.clone(),
pnl: None, metadata: HashMap::new(),
};
self.trade_log.push(entry);
}
fn update_metrics(&mut self) {
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let unrealized_pnl = self.simulated_positions.values()
.map(|p| p.unrealized_pnl)
.sum::<f64>();
let funding_pnl = self.simulated_positions.values()
.map(|p| p.funding_pnl)
.sum::<f64>();
self.metrics.current_balance = self.simulated_balance;
self.metrics.unrealized_pnl = unrealized_pnl;
self.metrics.funding_pnl = funding_pnl;
self.metrics.last_update = now;
let total_equity = self.simulated_balance + unrealized_pnl + funding_pnl;
if total_equity > self.metrics.peak_balance {
self.metrics.peak_balance = total_equity;
} else {
let drawdown = self.metrics.peak_balance - total_equity;
let drawdown_pct = if self.metrics.peak_balance > 0.0 {
drawdown / self.metrics.peak_balance * 100.0
} else {
0.0
};
if drawdown > self.metrics.max_drawdown {
self.metrics.max_drawdown = drawdown;
self.metrics.max_drawdown_pct = drawdown_pct;
}
}
}
pub fn generate_report(&self) -> PaperTradingReport {
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
let duration = now.signed_duration_since(self.metrics.start_time);
let duration_days = duration.num_milliseconds() as f64 / (1000.0 * 60.0 * 60.0 * 24.0);
let total_equity = self.simulated_balance + self.metrics.unrealized_pnl + self.metrics.funding_pnl;
let total_return = total_equity - self.metrics.initial_balance;
let total_return_pct = if self.metrics.initial_balance > 0.0 {
total_return / self.metrics.initial_balance * 100.0
} else {
0.0
};
let annualized_return = if duration_days > 0.0 {
(total_return_pct / 100.0 + 1.0).powf(365.0 / duration_days) - 1.0
} else {
0.0
} * 100.0;
let win_rate = if self.metrics.trade_count > 0 {
self.metrics.winning_trades as f64 / self.metrics.trade_count as f64 * 100.0
} else {
0.0
};
PaperTradingReport {
initial_balance: self.metrics.initial_balance,
current_balance: self.simulated_balance,
unrealized_pnl: self.metrics.unrealized_pnl,
realized_pnl: self.metrics.realized_pnl,
funding_pnl: self.metrics.funding_pnl,
total_pnl: self.metrics.realized_pnl + self.metrics.unrealized_pnl + self.metrics.funding_pnl,
total_fees: self.metrics.total_fees,
total_equity,
total_return,
total_return_pct,
annualized_return,
trade_count: self.metrics.trade_count,
winning_trades: self.metrics.winning_trades,
losing_trades: self.metrics.losing_trades,
win_rate,
max_drawdown: self.metrics.max_drawdown,
max_drawdown_pct: self.metrics.max_drawdown_pct,
start_time: self.metrics.start_time,
end_time: now,
duration_days,
}
}
}
#[derive(Debug, Clone)]
pub struct PaperTradingReport {
pub initial_balance: f64,
pub current_balance: f64,
pub unrealized_pnl: f64,
pub realized_pnl: f64,
pub funding_pnl: f64,
pub total_pnl: f64,
pub total_fees: f64,
pub total_equity: f64,
pub total_return: f64,
pub total_return_pct: f64,
pub annualized_return: f64,
pub trade_count: usize,
pub winning_trades: usize,
pub losing_trades: usize,
pub win_rate: f64,
pub max_drawdown: f64,
pub max_drawdown_pct: f64,
pub start_time: DateTime<FixedOffset>,
pub end_time: DateTime<FixedOffset>,
pub duration_days: f64,
}
impl std::fmt::Display for PaperTradingReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Paper Trading Performance Report ===")?;
writeln!(f, "Period: {} to {}", self.start_time, self.end_time)?;
writeln!(f, "Duration: {:.2} days", self.duration_days)?;
writeln!(f, "")?;
writeln!(f, "Initial Balance: ${:.2}", self.initial_balance)?;
writeln!(f, "Current Balance: ${:.2}", self.current_balance)?;
writeln!(f, "Unrealized P&L: ${:.2}", self.unrealized_pnl)?;
writeln!(f, "Realized P&L: ${:.2}", self.realized_pnl)?;
writeln!(f, "Funding P&L: ${:.2}", self.funding_pnl)?;
writeln!(f, "Total P&L: ${:.2}", self.total_pnl)?;
writeln!(f, "Total Fees: ${:.2}", self.total_fees)?;
writeln!(f, "")?;
writeln!(f, "Total Equity: ${:.2}", self.total_equity)?;
writeln!(f, "Total Return: ${:.2} ({:.2}%)", self.total_return, self.total_return_pct)?;
writeln!(f, "Annualized Return: {:.2}%", self.annualized_return)?;
writeln!(f, "")?;
writeln!(f, "Trade Count: {}", self.trade_count)?;
writeln!(f, "Winning Trades: {} ({:.2}%)", self.winning_trades, self.win_rate)?;
writeln!(f, "Losing Trades: {}", self.losing_trades)?;
writeln!(f, "Maximum Drawdown: ${:.2} ({:.2}%)", self.max_drawdown, self.max_drawdown_pct)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
struct MockStrategy {
name: String,
signals: HashMap<String, Signal>,
}
impl TradingStrategy for MockStrategy {
fn name(&self) -> &str {
&self.name
}
fn on_market_data(&mut self, data: &MarketData) -> Result<Vec<OrderRequest>, String> {
let symbol = &data.symbol;
let signal = self.signals.get(symbol);
match signal {
Some(signal) => {
match signal.direction {
SignalDirection::Buy => {
Ok(vec![OrderRequest::market(symbol, OrderSide::Buy, 1.0)])
},
SignalDirection::Sell => {
Ok(vec![OrderRequest::market(symbol, OrderSide::Sell, 1.0)])
},
_ => Ok(vec![]),
}
},
None => Ok(vec![]),
}
}
fn on_order_fill(&mut self, _fill: &OrderResult) -> Result<(), String> {
Ok(())
}
fn on_funding_payment(&mut self, _payment: &FundingPayment) -> Result<(), String> {
Ok(())
}
fn get_current_signals(&self) -> HashMap<String, Signal> {
self.signals.clone()
}
}
#[test]
fn test_paper_trading_engine_creation() {
let slippage_config = SlippageConfig::default();
let engine = PaperTradingEngine::new(10000.0, slippage_config);
assert_eq!(engine.simulated_balance, 10000.0);
assert!(engine.simulated_positions.is_empty());
assert!(engine.order_history.is_empty());
assert!(engine.active_orders.is_empty());
}
#[test]
fn test_market_data_update() {
let slippage_config = SlippageConfig::default();
let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
let now = Utc::now().with_timezone(&FixedOffset::east(0));
let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
let position = Position::new("BTC", 1.0, 49000.0, 49000.0, now);
engine.simulated_positions.insert("BTC".to_string(), position);
engine.update_market_data(market_data).unwrap();
let updated_position = engine.simulated_positions.get("BTC").unwrap();
assert_eq!(updated_position.current_price, 50000.0);
assert_eq!(updated_position.unrealized_pnl, 1000.0); }
#[test]
fn test_market_order_execution() {
let slippage_config = SlippageConfig {
base_slippage_pct: 0.0, volume_impact_factor: 0.0,
volatility_impact_factor: 0.0,
random_slippage_max_pct: 0.0,
simulated_latency_ms: 0,
use_order_book: false,
max_slippage_pct: 0.0,
};
let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
let now = Utc::now().with_timezone(&FixedOffset::east(0));
let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
engine.market_data_cache.insert("BTC".to_string(), market_data);
let order = OrderRequest::market("BTC", OrderSide::Buy, 0.1);
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(engine.execute_order(order)).unwrap();
assert_eq!(result.status, OrderStatus::Filled);
assert_eq!(result.filled_quantity, 0.1);
assert!(result.average_price.is_some());
assert!(result.fees.is_some());
let position = engine.simulated_positions.get("BTC").unwrap();
assert_eq!(position.size, 0.1);
assert_eq!(position.entry_price, 50010.0);
let fees = 0.1 * 50010.0 * engine.taker_fee;
assert_eq!(engine.simulated_balance, 10000.0 - (0.1 * 50010.0) - fees);
}
#[test]
fn test_limit_order_execution() {
let slippage_config = SlippageConfig::default();
let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
let now = Utc::now().with_timezone(&FixedOffset::east(0));
let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
engine.market_data_cache.insert("BTC".to_string(), market_data.clone());
let limit_price = 49980.0;
let order = OrderRequest::limit("BTC", OrderSide::Buy, 0.1, limit_price);
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(engine.execute_order(order)).unwrap();
assert_eq!(result.status, OrderStatus::Submitted);
assert_eq!(engine.active_orders.len(), 1);
let new_market_data = MarketData::new("BTC", 49970.0, 49960.0, 49980.0, 100.0, now);
engine.update_market_data(new_market_data).unwrap();
assert_eq!(engine.active_orders.len(), 0);
assert_eq!(engine.order_history.len(), 1);
let position = engine.simulated_positions.get("BTC").unwrap();
assert_eq!(position.size, 0.1);
assert_eq!(position.entry_price, limit_price);
}
#[test]
fn test_position_tracking() {
let slippage_config = SlippageConfig {
base_slippage_pct: 0.0, volume_impact_factor: 0.0,
volatility_impact_factor: 0.0,
random_slippage_max_pct: 0.0,
simulated_latency_ms: 0,
use_order_book: false,
max_slippage_pct: 0.0,
};
let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
let now = Utc::now().with_timezone(&FixedOffset::east(0));
let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
engine.market_data_cache.insert("BTC".to_string(), market_data);
let buy_order = OrderRequest::market("BTC", OrderSide::Buy, 0.1);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(engine.execute_order(buy_order)).unwrap();
let position = engine.simulated_positions.get("BTC").unwrap();
assert_eq!(position.size, 0.1);
let sell_order = OrderRequest::market("BTC", OrderSide::Sell, 0.05);
rt.block_on(engine.execute_order(sell_order)).unwrap();
let position = engine.simulated_positions.get("BTC").unwrap();
assert_eq!(position.size, 0.05);
let sell_order = OrderRequest::market("BTC", OrderSide::Sell, 0.05);
rt.block_on(engine.execute_order(sell_order)).unwrap();
let position = engine.simulated_positions.get("BTC").unwrap();
assert_eq!(position.size, 0.0);
}
#[test]
fn test_performance_metrics() {
let slippage_config = SlippageConfig {
base_slippage_pct: 0.0, volume_impact_factor: 0.0,
volatility_impact_factor: 0.0,
random_slippage_max_pct: 0.0,
simulated_latency_ms: 0,
use_order_book: false,
max_slippage_pct: 0.0,
};
let mut engine = PaperTradingEngine::new(10000.0, slippage_config);
let now = Utc::now().with_timezone(&FixedOffset::east(0));
let market_data = MarketData::new("BTC", 50000.0, 49990.0, 50010.0, 100.0, now);
engine.market_data_cache.insert("BTC".to_string(), market_data);
let buy_order = OrderRequest::market("BTC", OrderSide::Buy, 0.1);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(engine.execute_order(buy_order)).unwrap();
let new_market_data = MarketData::new("BTC", 51000.0, 50990.0, 51010.0, 100.0, now);
engine.update_market_data(new_market_data).unwrap();
let sell_order = OrderRequest::market("BTC", OrderSide::Sell, 0.1);
rt.block_on(engine.execute_order(sell_order)).unwrap();
let metrics = engine.get_metrics();
assert!(metrics.realized_pnl > 0.0); assert_eq!(metrics.trade_count, 1);
assert_eq!(metrics.winning_trades, 1);
let report = engine.generate_report();
assert!(report.total_return > 0.0);
assert!(report.total_return_pct > 0.0);
assert!(report.win_rate > 0.0);
}
}