BREAKING CHANGES: - Removed orphaned dqn.rs monolithic trainer (4,975 lines) - Removed orphaned dqn_ensemble.rs module (816 lines) - Removed orphaned tft.rs and tft_complete_int8_integration_test.rs - TFT trainer split into modular directory structure DQN Module Refactoring: - Split trainers/dqn.rs into modular structure (config.rs, statistics.rs, trainer.rs) - Fixed hyperopt 39D search space (continuous params only) - Boolean flags (use_dueling, use_double_dqn, use_per, use_noisy_nets) are now FIXED architectural decisions - use_distributional defaults to false (Candle BUG #36 - scatter_add gradient issues) Clean Module Structure: - ml/src/trainers/dqn/ directory with proper mod.rs exports - ml/src/trainers/tft/ directory with config.rs, types.rs, model.rs, trainer.rs, tests.rs - All P0 features validated: TD-error clamping, batch diversity, LR scheduler, priority staleness Documentation: - Added comprehensive docs in docs/codebase-cleanup/ - ADR-001 for DQN refactoring decisions - Rainbow DQN component matrix and quick reference guides Build Status: Compiles with zero errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1175 lines
31 KiB
Rust
1175 lines
31 KiB
Rust
//! Trading Engine Integration Tests
|
|
//!
|
|
//! Comprehensive test coverage for the core trading engine
|
|
//! Tests the full flow: order submission -> validation -> execution -> position updates
|
|
|
|
use chrono::Utc;
|
|
use common::{MarketDataEvent, OrderId, OrderSide, OrderStatus, OrderType};
|
|
use rust_decimal::Decimal;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{broadcast, RwLock};
|
|
use trading_engine::trading::data_interface::{DataProvider, Subscription};
|
|
use trading_engine::trading::engine::TradingEngine;
|
|
use trading_engine::trading_operations::{ExecutionResult, LiquidityFlag};
|
|
|
|
// ============================================================================
|
|
// Mock Data Provider for Testing
|
|
// ============================================================================
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct MockDataProvider {
|
|
market_data_tx: broadcast::Sender<MarketDataEvent>,
|
|
order_update_tx: broadcast::Sender<MarketDataEvent>,
|
|
subscriptions: Arc<RwLock<Vec<Subscription>>>,
|
|
}
|
|
|
|
impl MockDataProvider {
|
|
fn new() -> Self {
|
|
let (market_data_tx, _) = broadcast::channel(1000);
|
|
let (order_update_tx, _) = broadcast::channel(1000);
|
|
|
|
Self {
|
|
market_data_tx,
|
|
order_update_tx,
|
|
subscriptions: Arc::new(RwLock::new(Vec::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl DataProvider for MockDataProvider {
|
|
async fn subscribe_market_data(&self, subscription: Subscription) -> Result<(), String> {
|
|
self.subscriptions.write().await.push(subscription);
|
|
Ok(())
|
|
}
|
|
|
|
fn subscribe_market_data_events(&self) -> broadcast::Receiver<MarketDataEvent> {
|
|
self.market_data_tx.subscribe()
|
|
}
|
|
|
|
fn subscribe_order_update_events(&self) -> broadcast::Receiver<MarketDataEvent> {
|
|
self.order_update_tx.subscribe()
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
fn create_test_engine() -> TradingEngine {
|
|
let data_provider = Arc::new(MockDataProvider::new());
|
|
TradingEngine::new(data_provider)
|
|
}
|
|
|
|
fn create_execution(
|
|
order_id: OrderId,
|
|
symbol: &str,
|
|
quantity: i64,
|
|
price: i64,
|
|
) -> ExecutionResult {
|
|
ExecutionResult {
|
|
order_id,
|
|
symbol: symbol.to_string(),
|
|
executed_quantity: Decimal::from(quantity),
|
|
execution_price: Decimal::from(price),
|
|
execution_time: Utc::now(),
|
|
commission: Decimal::from(10),
|
|
liquidity_flag: LiquidityFlag::Maker,
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Order Submission Tests (10 scenarios)
|
|
// Note: These test validation logic before broker submission
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_market_order_buy_validation() {
|
|
let engine = create_test_engine();
|
|
|
|
// This will validate and create the order, but fail at broker submission
|
|
// which is expected without a configured broker
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
// Should fail because no broker is configured, but this proves validation passed
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("broker"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_limit_order_sell() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.submit_order(
|
|
"ETHUSD".to_string(),
|
|
OrderSide::Sell,
|
|
OrderType::Limit,
|
|
Decimal::from(10),
|
|
Some(Decimal::from(3000)),
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_order_zero_quantity_rejected() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::ZERO,
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("positive"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_order_negative_quantity_rejected() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(-10),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("positive"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_limit_order_zero_price_rejected() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(1),
|
|
Some(Decimal::ZERO),
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("price"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_order_empty_symbol_rejected() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.submit_order(
|
|
"".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("symbol"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_order_exceeds_buying_power() {
|
|
let engine = create_test_engine();
|
|
|
|
// Try to buy 10 BTC at 50k each (500k total) - exceeds demo account 100k buying power
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(10),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("buying power"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_multiple_orders_same_symbol() {
|
|
let engine = create_test_engine();
|
|
|
|
let result1 = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
let result2 = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Sell,
|
|
OrderType::Limit,
|
|
Decimal::from(1),
|
|
Some(Decimal::from(51000)),
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result1.is_ok());
|
|
assert!(result2.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_orders_different_symbols() {
|
|
let engine = create_test_engine();
|
|
|
|
let btc_result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
let eth_result = engine
|
|
.submit_order(
|
|
"ETHUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(btc_result.is_ok());
|
|
assert!(eth_result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_submit_order_within_buying_power_boundary() {
|
|
let engine = create_test_engine();
|
|
|
|
// Exactly at buying power limit (2 * 50000 = 100000)
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(2),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Order Cancellation Tests (5 scenarios)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_cancel_pending_order() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(1),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let result = engine.cancel_order(order_id).await;
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cancel_filled_order_rejected() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
// Simulate fill
|
|
let execution = create_execution(order_id, "BTCUSD", 1, 50000);
|
|
let _ = engine.process_execution(execution).await;
|
|
|
|
// Try to cancel filled order
|
|
let result = engine.cancel_order(order_id).await;
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("filled"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cancel_nonexistent_order() {
|
|
let engine = create_test_engine();
|
|
let fake_order_id: OrderId = "nonexistent".to_string().into();
|
|
|
|
let result = engine.cancel_order(fake_order_id).await;
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("not found"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cancel_partially_filled_order() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(10),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
// Partial fill (5 out of 10)
|
|
let execution = create_execution(order_id, "BTCUSD", 5, 50000);
|
|
let _ = engine.process_execution(execution).await;
|
|
|
|
// Should be able to cancel partially filled order
|
|
let result = engine.cancel_order(order_id).await;
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_order_status_after_submission() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(1),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let order = engine.get_order_status(order_id).await;
|
|
assert!(order.is_ok());
|
|
|
|
let order_data = order.unwrap();
|
|
assert_eq!(order_data.symbol, "BTCUSD");
|
|
assert_eq!(order_data.quantity, Decimal::from(1));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Fill Processing Tests (8 scenarios)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_process_full_execution() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution = create_execution(order_id, "BTCUSD", 1, 50000);
|
|
let result = engine.process_execution(execution).await;
|
|
assert!(result.is_ok());
|
|
|
|
// Verify order status is Filled
|
|
let order = engine.get_order_status(order_id).await.unwrap();
|
|
assert_eq!(order.status, OrderStatus::Filled);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_process_partial_execution() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(10),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
// First partial fill
|
|
let execution1 = create_execution(order_id, "BTCUSD", 3, 50000);
|
|
engine
|
|
.process_execution(execution1)
|
|
.await
|
|
.expect("First execution should succeed");
|
|
|
|
let order = engine.get_order_status(order_id).await.unwrap();
|
|
assert_eq!(order.status, OrderStatus::PartiallyFilled);
|
|
assert_eq!(order.fill_quantity, Decimal::from(3));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_process_multiple_partial_fills() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(100),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
// Multiple partial fills
|
|
let execution1 = create_execution(order_id, "BTCUSD", 30, 50000);
|
|
engine.process_execution(execution1).await.unwrap();
|
|
|
|
let execution2 = create_execution(order_id, "BTCUSD", 40, 50100);
|
|
engine.process_execution(execution2).await.unwrap();
|
|
|
|
let execution3 = create_execution(order_id, "BTCUSD", 30, 49900);
|
|
engine.process_execution(execution3).await.unwrap();
|
|
|
|
let order = engine.get_order_status(order_id).await.unwrap();
|
|
assert_eq!(order.status, OrderStatus::Filled);
|
|
assert_eq!(order.fill_quantity, Decimal::from(100));
|
|
|
|
// Check weighted average price
|
|
let expected_avg = (Decimal::from(30) * Decimal::from(50000)
|
|
+ Decimal::from(40) * Decimal::from(50100)
|
|
+ Decimal::from(30) * Decimal::from(49900))
|
|
/ Decimal::from(100);
|
|
|
|
assert_eq!(order.average_fill_price, Some(expected_avg));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execution_updates_position() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"ETHUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(10),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution = create_execution(order_id, "ETHUSD", 10, 3000);
|
|
engine.process_execution(execution).await.unwrap();
|
|
|
|
// Check position was created
|
|
let positions = engine.get_positions(Some("ETHUSD".to_string())).await;
|
|
assert!(positions.is_ok());
|
|
|
|
let pos_list = positions.unwrap();
|
|
assert_eq!(pos_list.len(), 1);
|
|
assert_eq!(pos_list[0].quantity, Decimal::from(10));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execution_updates_account_balance() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution = ExecutionResult {
|
|
order_id,
|
|
symbol: "BTCUSD".to_string(),
|
|
executed_quantity: Decimal::from(1),
|
|
execution_price: Decimal::from(50000),
|
|
execution_time: Utc::now(),
|
|
commission: Decimal::from(25), // $25 commission
|
|
liquidity_flag: LiquidityFlag::Taker,
|
|
};
|
|
|
|
engine.process_execution(execution).await.unwrap();
|
|
|
|
// Account should be updated (commission deducted)
|
|
let account = engine
|
|
.get_account_info("DEMO_ACCOUNT".to_string())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Cash balance should be reduced by commission
|
|
assert_eq!(
|
|
account.cash_balance,
|
|
Decimal::from(50000) - Decimal::from(25)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execution_with_slippage() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(1),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
// Execute at worse price (slippage)
|
|
let execution = create_execution(order_id, "BTCUSD", 1, 50100);
|
|
let result = engine.process_execution(execution).await;
|
|
assert!(result.is_ok());
|
|
|
|
let order = engine.get_order_status(order_id).await.unwrap();
|
|
assert_eq!(order.average_fill_price, Some(Decimal::from(50100)));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execution_for_nonexistent_order() {
|
|
let engine = create_test_engine();
|
|
|
|
let fake_order_id: OrderId = "nonexistent".to_string().into();
|
|
let execution = create_execution(fake_order_id, "BTCUSD", 1, 50000);
|
|
|
|
let result = engine.process_execution(execution).await;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execution_creates_position_if_not_exists() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"SOLUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(100),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution = create_execution(order_id, "SOLUSD", 100, 150);
|
|
engine.process_execution(execution).await.unwrap();
|
|
|
|
let positions = engine.get_positions(Some("SOLUSD".to_string())).await;
|
|
assert!(positions.is_ok());
|
|
|
|
let pos_list = positions.unwrap();
|
|
assert_eq!(pos_list.len(), 1);
|
|
assert_eq!(pos_list[0].symbol.to_string(), "SOLUSD");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Position Management Tests (7 scenarios)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_get_positions_empty() {
|
|
let engine = create_test_engine();
|
|
|
|
let positions = engine.get_positions(None).await;
|
|
assert!(positions.is_ok());
|
|
assert!(positions.unwrap().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_positions_after_execution() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(2),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution = create_execution(order_id, "BTCUSD", 2, 50000);
|
|
engine.process_execution(execution).await.unwrap();
|
|
|
|
let positions = engine.get_positions(None).await.unwrap();
|
|
assert_eq!(positions.len(), 1);
|
|
assert_eq!(positions[0].symbol.to_string(), "BTCUSD");
|
|
assert_eq!(positions[0].quantity, Decimal::from(2));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_positions_multiple_symbols() {
|
|
let engine = create_test_engine();
|
|
|
|
// BTC position
|
|
let btc_order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let btc_execution = create_execution(btc_order_id, "BTCUSD", 1, 50000);
|
|
engine.process_execution(btc_execution).await.unwrap();
|
|
|
|
// ETH position
|
|
let eth_order_id: OrderId = engine
|
|
.submit_order(
|
|
"ETHUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(10),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let eth_execution = create_execution(eth_order_id, "ETHUSD", 10, 3000);
|
|
engine.process_execution(eth_execution).await.unwrap();
|
|
|
|
let positions = engine.get_positions(None).await.unwrap();
|
|
assert_eq!(positions.len(), 2);
|
|
|
|
let symbols: Vec<String> = positions.iter().map(|p| p.symbol.to_string()).collect();
|
|
assert!(symbols.contains(&"BTCUSD".to_string()));
|
|
assert!(symbols.contains(&"ETHUSD".to_string()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_positions_filtered_by_symbol() {
|
|
let engine = create_test_engine();
|
|
|
|
// Create multiple positions
|
|
let btc_order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let btc_execution = create_execution(btc_order_id, "BTCUSD", 1, 50000);
|
|
engine.process_execution(btc_execution).await.unwrap();
|
|
|
|
let eth_order_id: OrderId = engine
|
|
.submit_order(
|
|
"ETHUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(10),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let eth_execution = create_execution(eth_order_id, "ETHUSD", 10, 3000);
|
|
engine.process_execution(eth_execution).await.unwrap();
|
|
|
|
// Filter for BTC only
|
|
let btc_positions = engine
|
|
.get_positions(Some("BTCUSD".to_string()))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(btc_positions.len(), 1);
|
|
assert_eq!(btc_positions[0].symbol.to_string(), "BTCUSD");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_position_increases_with_additional_buy() {
|
|
let engine = create_test_engine();
|
|
|
|
// First buy
|
|
let order1_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution1 = create_execution(order1_id, "BTCUSD", 1, 50000);
|
|
engine.process_execution(execution1).await.unwrap();
|
|
|
|
// Second buy
|
|
let order2_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution2 = create_execution(order2_id, "BTCUSD", 1, 51000);
|
|
engine.process_execution(execution2).await.unwrap();
|
|
|
|
let positions = engine
|
|
.get_positions(Some("BTCUSD".to_string()))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(positions.len(), 1);
|
|
assert_eq!(positions[0].quantity, Decimal::from(2));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_position_reduces_with_sell() {
|
|
let engine = create_test_engine();
|
|
|
|
// Buy 10
|
|
let buy_order_id: OrderId = engine
|
|
.submit_order(
|
|
"ETHUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(10),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let buy_execution = create_execution(buy_order_id, "ETHUSD", 10, 3000);
|
|
engine.process_execution(buy_execution).await.unwrap();
|
|
|
|
// Sell 6
|
|
let sell_order_id: OrderId = engine
|
|
.submit_order(
|
|
"ETHUSD".to_string(),
|
|
OrderSide::Sell,
|
|
OrderType::Market,
|
|
Decimal::from(6),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let sell_execution = ExecutionResult {
|
|
order_id: sell_order_id,
|
|
symbol: "ETHUSD".to_string(),
|
|
executed_quantity: Decimal::from(-6), // Negative for sell
|
|
execution_price: Decimal::from(3100),
|
|
execution_time: Utc::now(),
|
|
commission: Decimal::from(10),
|
|
liquidity_flag: LiquidityFlag::Taker,
|
|
};
|
|
|
|
engine.process_execution(sell_execution).await.unwrap();
|
|
|
|
let positions = engine
|
|
.get_positions(Some("ETHUSD".to_string()))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(positions.len(), 1);
|
|
assert_eq!(positions[0].quantity, Decimal::from(4));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_position_flattened_with_equal_sell() {
|
|
let engine = create_test_engine();
|
|
|
|
// Buy 10
|
|
let buy_order_id: OrderId = engine
|
|
.submit_order(
|
|
"SOLUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(10),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let buy_execution = create_execution(buy_order_id, "SOLUSD", 10, 100);
|
|
engine.process_execution(buy_execution).await.unwrap();
|
|
|
|
// Sell 10 (flatten)
|
|
let sell_order_id: OrderId = engine
|
|
.submit_order(
|
|
"SOLUSD".to_string(),
|
|
OrderSide::Sell,
|
|
OrderType::Market,
|
|
Decimal::from(10),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let sell_execution = ExecutionResult {
|
|
order_id: sell_order_id,
|
|
symbol: "SOLUSD".to_string(),
|
|
executed_quantity: Decimal::from(-10),
|
|
execution_price: Decimal::from(110),
|
|
execution_time: Utc::now(),
|
|
commission: Decimal::from(5),
|
|
liquidity_flag: LiquidityFlag::Maker,
|
|
};
|
|
|
|
engine.process_execution(sell_execution).await.unwrap();
|
|
|
|
let positions = engine
|
|
.get_positions(Some("SOLUSD".to_string()))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(positions.len(), 1);
|
|
assert_eq!(positions[0].quantity, Decimal::ZERO);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Market Data Subscription Tests (3 scenarios)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_subscribe_market_data() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.subscribe_market_data(vec!["BTCUSD".to_string(), "ETHUSD".to_string()])
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_subscribe_order_updates() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine.subscribe_order_updates(None).await;
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_subscribe_order_updates_with_account() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.subscribe_order_updates(Some("DEMO_ACCOUNT".to_string()))
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Trading Stats Tests (2 scenarios)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_get_trading_stats_initial() {
|
|
let engine = create_test_engine();
|
|
|
|
let stats = engine.get_trading_stats().await;
|
|
|
|
// Initial stats should have zeros
|
|
assert_eq!(stats.total_orders, 0);
|
|
assert_eq!(stats.filled_orders, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_trading_stats_after_orders() {
|
|
let engine = create_test_engine();
|
|
|
|
// Submit and execute an order
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
let execution = create_execution(order_id, "BTCUSD", 1, 50000);
|
|
engine.process_execution(execution).await.unwrap();
|
|
|
|
let stats = engine.get_trading_stats().await;
|
|
|
|
assert_eq!(stats.total_orders, 1);
|
|
assert_eq!(stats.filled_orders, 1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Account Info Tests (2 scenarios)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_get_account_info_demo_account() {
|
|
let engine = create_test_engine();
|
|
|
|
let account = engine
|
|
.get_account_info("DEMO_ACCOUNT".to_string())
|
|
.await;
|
|
|
|
assert!(account.is_ok());
|
|
|
|
let account_info = account.unwrap();
|
|
assert_eq!(account_info.account_id, "DEMO_ACCOUNT");
|
|
assert_eq!(account_info.total_value, Decimal::from(100000));
|
|
assert_eq!(account_info.cash_balance, Decimal::from(50000));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_account_info_nonexistent() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.get_account_info("NONEXISTENT".to_string())
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("not found"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edge Cases & Error Handling (5 scenarios)
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_order_submissions() {
|
|
let engine = Arc::new(create_test_engine());
|
|
|
|
let mut handles = vec![];
|
|
|
|
for i in 0..5 {
|
|
let engine_clone = Arc::clone(&engine);
|
|
let handle = tokio::spawn(async move {
|
|
engine_clone
|
|
.submit_order(
|
|
format!("SYM{}", i),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::from(1),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
let results: Vec<_> = futures::future::join_all(handles).await;
|
|
|
|
// All should succeed
|
|
for result in results {
|
|
assert!(result.unwrap().is_ok());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_rapid_fire_executions() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(100),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
// Process 10 small executions rapidly
|
|
for i in 1..=10 {
|
|
let execution = create_execution(order_id, "BTCUSD", 10, 50000 + i * 10);
|
|
engine.process_execution(execution).await.unwrap();
|
|
}
|
|
|
|
let order = engine.get_order_status(order_id).await.unwrap();
|
|
assert_eq!(order.status, OrderStatus::Filled);
|
|
assert_eq!(order.fill_quantity, Decimal::from(100));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_order_after_partial_cancel() {
|
|
let engine = create_test_engine();
|
|
|
|
let order_id: OrderId = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(10),
|
|
Some(Decimal::from(50000)),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into();
|
|
|
|
// Partial fill
|
|
let execution = create_execution(order_id, "BTCUSD", 3, 50000);
|
|
engine.process_execution(execution).await.unwrap();
|
|
|
|
// Cancel the rest
|
|
engine.cancel_order(order_id).await.unwrap();
|
|
|
|
let order = engine.get_order_status(order_id).await.unwrap();
|
|
assert_eq!(order.status, OrderStatus::Cancelled);
|
|
assert_eq!(order.fill_quantity, Decimal::from(3)); // Partial fill remains
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_large_order_quantity() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Limit,
|
|
Decimal::from(1000000),
|
|
Some(Decimal::from(1)), // Low price to pass buying power check
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_fractional_order_quantity() {
|
|
let engine = create_test_engine();
|
|
|
|
let result = engine
|
|
.submit_order(
|
|
"BTCUSD".to_string(),
|
|
OrderSide::Buy,
|
|
OrderType::Market,
|
|
Decimal::new(15, 1), // 1.5
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
}
|