Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade Files updated: - Cargo.lock: Dependency resolution for Tonic 0.14.2 - All build.rs: Updated for tonic-prost-build - Proto files: Regenerated with tonic-prost 0.14 - Examples/tests: Updated for new gRPC API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
940 lines
29 KiB
Rust
940 lines
29 KiB
Rust
//! Mock Services for Foxhunt HFT Trading System Testing
|
|
//!
|
|
//! This module provides mock implementations of all external services
|
|
//! for isolated testing without dependencies on real services.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::sync::{mpsc, RwLock, Mutex};
|
|
use tokio::time::sleep;
|
|
use uuid::Uuid;
|
|
use chrono::{DateTime, Utc};
|
|
use rust_decimal::Decimal;
|
|
|
|
use super::test_config::TestConfig;
|
|
|
|
// =============================================================================
|
|
// MOCK TRADING SERVICE
|
|
// =============================================================================
|
|
|
|
/// Mock trading service for testing trading operations
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockTradingService {
|
|
_config: TestConfig,
|
|
orders: Arc<RwLock<HashMap<Uuid, MockOrder>>>,
|
|
positions: Arc<RwLock<HashMap<String, MockPosition>>>,
|
|
order_events: Arc<Mutex<mpsc::UnboundedSender<MockOrderEvent>>>,
|
|
latency_ms: u64,
|
|
failure_rate: f64,
|
|
next_order_id: Arc<RwLock<u64>>,
|
|
}
|
|
|
|
impl MockTradingService {
|
|
pub fn new(config: TestConfig) -> Self {
|
|
let (sender, _receiver) = mpsc::unbounded_channel();
|
|
let latency_ms = config.services.mock_latency_ms;
|
|
let failure_rate = config.services.mock_failure_rate;
|
|
|
|
Self {
|
|
latency_ms,
|
|
failure_rate,
|
|
_config: config,
|
|
orders: Arc::new(RwLock::new(HashMap::new())),
|
|
positions: Arc::new(RwLock::new(HashMap::new())),
|
|
order_events: Arc::new(Mutex::new(sender)),
|
|
next_order_id: Arc::new(RwLock::new(1)),
|
|
}
|
|
}
|
|
|
|
/// Submit a new order
|
|
pub async fn submit_order(&self, request: MockOrderRequest) -> Result<MockOrderResponse, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let order_id = {
|
|
let mut next_id = self.next_order_id.write().await;
|
|
let id = *next_id;
|
|
*next_id += 1;
|
|
id
|
|
};
|
|
|
|
let order = MockOrder {
|
|
id: Uuid::new_v4(),
|
|
order_id,
|
|
symbol: request.symbol.clone(),
|
|
side: request.side,
|
|
quantity: request.quantity,
|
|
price: request.price,
|
|
order_type: request.order_type,
|
|
status: MockOrderStatus::Pending,
|
|
filled_quantity: Decimal::ZERO,
|
|
average_fill_price: Decimal::ZERO,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
};
|
|
|
|
// Store the order
|
|
self.orders.write().await.insert(order.id, order.clone());
|
|
|
|
// Simulate order processing in background
|
|
let service = self.clone();
|
|
let order_id = order.id;
|
|
tokio::spawn(async move {
|
|
service.process_order(order_id).await;
|
|
});
|
|
|
|
Ok(MockOrderResponse {
|
|
order_id: order.id,
|
|
status: MockOrderStatus::Pending,
|
|
message: "Order submitted successfully".to_string(),
|
|
})
|
|
}
|
|
|
|
/// Cancel an existing order
|
|
pub async fn cancel_order(&self, order_id: Uuid) -> Result<MockOrderResponse, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let mut orders = self.orders.write().await;
|
|
if let Some(order) = orders.get_mut(&order_id) {
|
|
if matches!(order.status, MockOrderStatus::Pending | MockOrderStatus::PartiallyFilled) {
|
|
order.status = MockOrderStatus::Cancelled;
|
|
order.updated_at = Utc::now();
|
|
|
|
Ok(MockOrderResponse {
|
|
order_id,
|
|
status: MockOrderStatus::Cancelled,
|
|
message: "Order cancelled successfully".to_string(),
|
|
})
|
|
} else {
|
|
Err(MockServiceError::InvalidOperation(
|
|
format!("Cannot cancel order in status: {:?}", order.status)
|
|
))
|
|
}
|
|
} else {
|
|
Err(MockServiceError::OrderNotFound(order_id))
|
|
}
|
|
}
|
|
|
|
/// Get order status
|
|
pub async fn get_order_status(&self, order_id: Uuid) -> Result<MockOrder, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let orders = self.orders.read().await;
|
|
orders.get(&order_id)
|
|
.cloned()
|
|
.ok_or(MockServiceError::OrderNotFound(order_id))
|
|
}
|
|
|
|
/// Get all orders for a symbol
|
|
pub async fn get_orders_for_symbol(&self, symbol: &str) -> Result<Vec<MockOrder>, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let orders = self.orders.read().await;
|
|
let filtered_orders: Vec<MockOrder> = orders
|
|
.values()
|
|
.filter(|order| order.symbol == symbol)
|
|
.cloned()
|
|
.collect();
|
|
|
|
Ok(filtered_orders)
|
|
}
|
|
|
|
/// Get current positions
|
|
pub async fn get_positions(&self) -> Result<Vec<MockPosition>, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let positions = self.positions.read().await;
|
|
Ok(positions.values().cloned().collect())
|
|
}
|
|
|
|
/// Process order asynchronously (simulate fill)
|
|
async fn process_order(&self, order_id: Uuid) {
|
|
// Wait a bit before processing
|
|
sleep(Duration::from_millis(100 + self.latency_ms)).await;
|
|
|
|
let mut orders = self.orders.write().await;
|
|
if let Some(order) = orders.get_mut(&order_id) {
|
|
// Simulate partial fill first
|
|
order.status = MockOrderStatus::PartiallyFilled;
|
|
order.filled_quantity = order.quantity / Decimal::from(2); // Fill 50%
|
|
order.average_fill_price = order.price;
|
|
order.updated_at = Utc::now();
|
|
|
|
// Send event
|
|
self.send_order_event(order.clone()).await;
|
|
|
|
// Wait a bit more
|
|
drop(orders); // Release lock
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// Complete the fill
|
|
let mut orders = self.orders.write().await;
|
|
if let Some(order) = orders.get_mut(&order_id) {
|
|
order.status = MockOrderStatus::Filled;
|
|
order.filled_quantity = order.quantity;
|
|
order.updated_at = Utc::now();
|
|
|
|
// Update position
|
|
self.update_position(order).await;
|
|
|
|
// Send final event
|
|
self.send_order_event(order.clone()).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Update position based on filled order
|
|
async fn update_position(&self, order: &MockOrder) {
|
|
let mut positions = self.positions.write().await;
|
|
let position_key = order.symbol.clone();
|
|
|
|
let position = positions.entry(position_key).or_insert_with(|| MockPosition {
|
|
symbol: order.symbol.clone(),
|
|
quantity: Decimal::ZERO,
|
|
average_price: Decimal::ZERO,
|
|
market_value: Decimal::ZERO,
|
|
unrealized_pnl: Decimal::ZERO,
|
|
updated_at: Utc::now(),
|
|
});
|
|
|
|
// Update position quantity and average price
|
|
let old_quantity = position.quantity;
|
|
let old_value = old_quantity * position.average_price;
|
|
|
|
let order_quantity = match order.side {
|
|
MockOrderSide::Buy => order.filled_quantity,
|
|
MockOrderSide::Sell => -order.filled_quantity,
|
|
};
|
|
|
|
let new_quantity = old_quantity + order_quantity;
|
|
let order_value = order.filled_quantity * order.average_fill_price;
|
|
|
|
if new_quantity != Decimal::ZERO {
|
|
let new_total_value = match order.side {
|
|
MockOrderSide::Buy => old_value + order_value,
|
|
MockOrderSide::Sell => old_value - order_value,
|
|
};
|
|
position.average_price = new_total_value / new_quantity.abs();
|
|
} else {
|
|
position.average_price = Decimal::ZERO;
|
|
}
|
|
|
|
position.quantity = new_quantity;
|
|
position.market_value = position.quantity * order.average_fill_price; // Assume market price = fill price
|
|
position.unrealized_pnl = (order.average_fill_price - position.average_price) * position.quantity;
|
|
position.updated_at = Utc::now();
|
|
}
|
|
|
|
/// Send order event
|
|
async fn send_order_event(&self, order: MockOrder) {
|
|
let event = MockOrderEvent {
|
|
order_id: order.id,
|
|
symbol: order.symbol,
|
|
status: order.status,
|
|
filled_quantity: order.filled_quantity,
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
if let Ok(sender) = self.order_events.try_lock() {
|
|
let _ = sender.send(event);
|
|
}
|
|
}
|
|
|
|
async fn simulate_latency(&self) {
|
|
if self.latency_ms > 0 {
|
|
sleep(Duration::from_millis(self.latency_ms)).await;
|
|
}
|
|
}
|
|
|
|
fn simulate_failure(&self) -> Result<(), MockServiceError> {
|
|
if self.failure_rate > 0.0 {
|
|
use rand::Rng;
|
|
let mut rng = rand::thread_rng();
|
|
if rng.gen::<f64>() < self.failure_rate {
|
|
return Err(MockServiceError::SimulatedFailure);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// MOCK ML TRAINING SERVICE
|
|
// =============================================================================
|
|
|
|
/// Mock ML training service for testing ML operations
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockMLTrainingService {
|
|
_config: TestConfig,
|
|
training_jobs: Arc<RwLock<HashMap<Uuid, MockTrainingJob>>>,
|
|
models: Arc<RwLock<HashMap<String, MockModel>>>,
|
|
latency_ms: u64,
|
|
failure_rate: f64,
|
|
}
|
|
|
|
impl MockMLTrainingService {
|
|
pub fn new(config: TestConfig) -> Self {
|
|
let latency_ms = config.services.mock_latency_ms;
|
|
let failure_rate = config.services.mock_failure_rate;
|
|
|
|
Self {
|
|
latency_ms,
|
|
failure_rate,
|
|
_config: config,
|
|
training_jobs: Arc::new(RwLock::new(HashMap::new())),
|
|
models: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
/// Start a new training job
|
|
pub async fn start_training(&self, request: MockTrainingRequest) -> Result<MockTrainingResponse, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let job_id = Uuid::new_v4();
|
|
let job = MockTrainingJob {
|
|
id: job_id,
|
|
model_name: request.model_name.clone(),
|
|
model_type: request.model_type,
|
|
status: MockTrainingStatus::Running,
|
|
progress: 0.0,
|
|
start_time: Utc::now(),
|
|
end_time: None,
|
|
metrics: HashMap::new(),
|
|
error_message: None,
|
|
};
|
|
|
|
self.training_jobs.write().await.insert(job_id, job.clone());
|
|
|
|
// Simulate training in background
|
|
let service = self.clone();
|
|
tokio::spawn(async move {
|
|
service.simulate_training(job_id).await;
|
|
});
|
|
|
|
Ok(MockTrainingResponse {
|
|
job_id,
|
|
status: MockTrainingStatus::Running,
|
|
message: "Training job started".to_string(),
|
|
})
|
|
}
|
|
|
|
/// Get training job status
|
|
pub async fn get_training_status(&self, job_id: Uuid) -> Result<MockTrainingJob, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let jobs = self.training_jobs.read().await;
|
|
jobs.get(&job_id)
|
|
.cloned()
|
|
.ok_or(MockServiceError::JobNotFound(job_id))
|
|
}
|
|
|
|
/// Get available models
|
|
pub async fn get_models(&self) -> Result<Vec<MockModel>, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let models = self.models.read().await;
|
|
Ok(models.values().cloned().collect())
|
|
}
|
|
|
|
/// Run inference with a model
|
|
pub async fn run_inference(&self, request: MockInferenceRequest) -> Result<MockInferenceResponse, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
// Simulate inference calculation
|
|
let prediction = match request.model_name.as_str() {
|
|
"momentum_model" => 0.75, // Bullish
|
|
"mean_reversion_model" => -0.25, // Bearish
|
|
_ => 0.0, // Neutral
|
|
};
|
|
|
|
Ok(MockInferenceResponse {
|
|
prediction,
|
|
confidence: 0.85,
|
|
features_used: request.features.len(),
|
|
inference_time_ms: self.latency_ms,
|
|
})
|
|
}
|
|
|
|
/// Simulate training process
|
|
async fn simulate_training(&self, job_id: Uuid) {
|
|
let training_duration = Duration::from_secs(5); // Simulate 5 second training
|
|
let update_interval = Duration::from_millis(500);
|
|
let total_updates = training_duration.as_millis() / update_interval.as_millis();
|
|
|
|
for i in 0..total_updates {
|
|
sleep(update_interval).await;
|
|
|
|
let progress = (i as f64 + 1.0) / total_updates as f64;
|
|
|
|
// Update job progress
|
|
if let Ok(mut jobs) = self.training_jobs.try_write() {
|
|
if let Some(job) = jobs.get_mut(&job_id) {
|
|
job.progress = progress;
|
|
|
|
// Add some metrics
|
|
job.metrics.insert("loss".to_string(), 1.0 - (progress * 0.8));
|
|
job.metrics.insert("accuracy".to_string(), 0.5 + (progress * 0.4));
|
|
|
|
if progress >= 1.0 {
|
|
job.status = MockTrainingStatus::Completed;
|
|
job.end_time = Some(Utc::now());
|
|
|
|
// Save the trained model
|
|
let model = MockModel {
|
|
name: job.model_name.clone(),
|
|
model_type: job.model_type,
|
|
version: "1.0.0".to_string(),
|
|
accuracy: job.metrics.get("accuracy").copied().unwrap_or(0.9),
|
|
created_at: Utc::now(),
|
|
file_path: format!("/models/{}_v1.0.0.bin", job.model_name),
|
|
};
|
|
|
|
if let Ok(mut models) = self.models.try_write() {
|
|
models.insert(model.name.clone(), model);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn simulate_latency(&self) {
|
|
if self.latency_ms > 0 {
|
|
sleep(Duration::from_millis(self.latency_ms)).await;
|
|
}
|
|
}
|
|
|
|
fn simulate_failure(&self) -> Result<(), MockServiceError> {
|
|
if self.failure_rate > 0.0 {
|
|
use rand::Rng;
|
|
let mut rng = rand::thread_rng();
|
|
if rng.gen::<f64>() < self.failure_rate {
|
|
return Err(MockServiceError::SimulatedFailure);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// MOCK BACKTESTING SERVICE
|
|
// =============================================================================
|
|
|
|
/// Mock backtesting service for testing strategy backtests
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockBacktestingService {
|
|
_config: TestConfig,
|
|
backtests: Arc<RwLock<HashMap<Uuid, MockBacktest>>>,
|
|
latency_ms: u64,
|
|
failure_rate: f64,
|
|
}
|
|
|
|
impl MockBacktestingService {
|
|
pub fn new(config: TestConfig) -> Self {
|
|
let latency_ms = config.services.mock_latency_ms;
|
|
let failure_rate = config.services.mock_failure_rate;
|
|
|
|
Self {
|
|
latency_ms,
|
|
failure_rate,
|
|
_config: config,
|
|
backtests: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
/// Start a new backtest
|
|
pub async fn start_backtest(&self, request: MockBacktestRequest) -> Result<MockBacktestResponse, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let backtest_id = Uuid::new_v4();
|
|
let backtest = MockBacktest {
|
|
id: backtest_id,
|
|
strategy_name: request.strategy_name.clone(),
|
|
symbols: request.symbols,
|
|
start_date: request.start_date,
|
|
end_date: request.end_date,
|
|
status: MockBacktestStatus::Running,
|
|
progress: 0.0,
|
|
results: None,
|
|
start_time: Utc::now(),
|
|
end_time: None,
|
|
};
|
|
|
|
self.backtests.write().await.insert(backtest_id, backtest);
|
|
|
|
// Simulate backtest in background
|
|
let service = self.clone();
|
|
tokio::spawn(async move {
|
|
service.simulate_backtest(backtest_id).await;
|
|
});
|
|
|
|
Ok(MockBacktestResponse {
|
|
backtest_id,
|
|
status: MockBacktestStatus::Running,
|
|
message: "Backtest started".to_string(),
|
|
})
|
|
}
|
|
|
|
/// Get backtest status
|
|
pub async fn get_backtest_status(&self, backtest_id: Uuid) -> Result<MockBacktest, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let backtests = self.backtests.read().await;
|
|
backtests.get(&backtest_id)
|
|
.cloned()
|
|
.ok_or(MockServiceError::BacktestNotFound(backtest_id))
|
|
}
|
|
|
|
/// Get all backtests
|
|
pub async fn get_backtests(&self) -> Result<Vec<MockBacktest>, MockServiceError> {
|
|
self.simulate_latency().await;
|
|
self.simulate_failure()?;
|
|
|
|
let backtests = self.backtests.read().await;
|
|
Ok(backtests.values().cloned().collect())
|
|
}
|
|
|
|
/// Simulate backtest process
|
|
async fn simulate_backtest(&self, backtest_id: Uuid) {
|
|
let backtest_duration = Duration::from_secs(3); // Simulate 3 second backtest
|
|
let update_interval = Duration::from_millis(300);
|
|
let total_updates = backtest_duration.as_millis() / update_interval.as_millis();
|
|
|
|
for i in 0..total_updates {
|
|
sleep(update_interval).await;
|
|
|
|
let progress = (i as f64 + 1.0) / total_updates as f64;
|
|
|
|
if let Ok(mut backtests) = self.backtests.try_write() {
|
|
if let Some(backtest) = backtests.get_mut(&backtest_id) {
|
|
backtest.progress = progress;
|
|
|
|
if progress >= 1.0 {
|
|
backtest.status = MockBacktestStatus::Completed;
|
|
backtest.end_time = Some(Utc::now());
|
|
|
|
// Generate mock results
|
|
backtest.results = Some(MockBacktestResults {
|
|
total_return: 0.15, // 15% return
|
|
annualized_return: 0.12,
|
|
volatility: 0.18,
|
|
sharpe_ratio: 0.67,
|
|
max_drawdown: 0.08,
|
|
win_rate: 0.58,
|
|
total_trades: 1250,
|
|
profit_factor: 1.35,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn simulate_latency(&self) {
|
|
if self.latency_ms > 0 {
|
|
sleep(Duration::from_millis(self.latency_ms)).await;
|
|
}
|
|
}
|
|
|
|
fn simulate_failure(&self) -> Result<(), MockServiceError> {
|
|
if self.failure_rate > 0.0 {
|
|
use rand::Rng;
|
|
let mut rng = rand::thread_rng();
|
|
if rng.gen::<f64>() < self.failure_rate {
|
|
return Err(MockServiceError::SimulatedFailure);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// DATA STRUCTURES
|
|
// =============================================================================
|
|
|
|
/// Mock order structure
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockOrder {
|
|
pub id: Uuid,
|
|
pub order_id: u64,
|
|
pub symbol: String,
|
|
pub side: MockOrderSide,
|
|
pub quantity: Decimal,
|
|
pub price: Decimal,
|
|
pub order_type: MockOrderType,
|
|
pub status: MockOrderStatus,
|
|
pub filled_quantity: Decimal,
|
|
pub average_fill_price: Decimal,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MockOrderSide {
|
|
Buy,
|
|
Sell,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MockOrderType {
|
|
Market,
|
|
Limit,
|
|
Stop,
|
|
StopLimit,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MockOrderStatus {
|
|
Pending,
|
|
PartiallyFilled,
|
|
Filled,
|
|
Cancelled,
|
|
Rejected,
|
|
}
|
|
|
|
/// Mock position structure
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockPosition {
|
|
pub symbol: String,
|
|
pub quantity: Decimal,
|
|
pub average_price: Decimal,
|
|
pub market_value: Decimal,
|
|
pub unrealized_pnl: Decimal,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Mock order request
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockOrderRequest {
|
|
pub symbol: String,
|
|
pub side: MockOrderSide,
|
|
pub quantity: Decimal,
|
|
pub price: Decimal,
|
|
pub order_type: MockOrderType,
|
|
}
|
|
|
|
/// Mock order response
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockOrderResponse {
|
|
pub order_id: Uuid,
|
|
pub status: MockOrderStatus,
|
|
pub message: String,
|
|
}
|
|
|
|
/// Mock order event
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockOrderEvent {
|
|
pub order_id: Uuid,
|
|
pub symbol: String,
|
|
pub status: MockOrderStatus,
|
|
pub filled_quantity: Decimal,
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Mock training job
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockTrainingJob {
|
|
pub id: Uuid,
|
|
pub model_name: String,
|
|
pub model_type: MockModelType,
|
|
pub status: MockTrainingStatus,
|
|
pub progress: f64,
|
|
pub start_time: DateTime<Utc>,
|
|
pub end_time: Option<DateTime<Utc>>,
|
|
pub metrics: HashMap<String, f64>,
|
|
pub error_message: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MockModelType {
|
|
Mamba,
|
|
Transformer,
|
|
DQN,
|
|
PPO,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MockTrainingStatus {
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
Cancelled,
|
|
}
|
|
|
|
/// Mock model
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockModel {
|
|
pub name: String,
|
|
pub model_type: MockModelType,
|
|
pub version: String,
|
|
pub accuracy: f64,
|
|
pub created_at: DateTime<Utc>,
|
|
pub file_path: String,
|
|
}
|
|
|
|
/// Mock training request
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockTrainingRequest {
|
|
pub model_name: String,
|
|
pub model_type: MockModelType,
|
|
pub data_path: String,
|
|
pub hyperparameters: HashMap<String, serde_json::Value>,
|
|
}
|
|
|
|
/// Mock training response
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockTrainingResponse {
|
|
pub job_id: Uuid,
|
|
pub status: MockTrainingStatus,
|
|
pub message: String,
|
|
}
|
|
|
|
/// Mock inference request
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockInferenceRequest {
|
|
pub model_name: String,
|
|
pub features: Vec<f64>,
|
|
}
|
|
|
|
/// Mock inference response
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockInferenceResponse {
|
|
pub prediction: f64,
|
|
pub confidence: f64,
|
|
pub features_used: usize,
|
|
pub inference_time_ms: u64,
|
|
}
|
|
|
|
/// Mock backtest
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockBacktest {
|
|
pub id: Uuid,
|
|
pub strategy_name: String,
|
|
pub symbols: Vec<String>,
|
|
pub start_date: DateTime<Utc>,
|
|
pub end_date: DateTime<Utc>,
|
|
pub status: MockBacktestStatus,
|
|
pub progress: f64,
|
|
pub results: Option<MockBacktestResults>,
|
|
pub start_time: DateTime<Utc>,
|
|
pub end_time: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MockBacktestStatus {
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
Cancelled,
|
|
}
|
|
|
|
/// Mock backtest results
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockBacktestResults {
|
|
pub total_return: f64,
|
|
pub annualized_return: f64,
|
|
pub volatility: f64,
|
|
pub sharpe_ratio: f64,
|
|
pub max_drawdown: f64,
|
|
pub win_rate: f64,
|
|
pub total_trades: u64,
|
|
pub profit_factor: f64,
|
|
}
|
|
|
|
/// Mock backtest request
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockBacktestRequest {
|
|
pub strategy_name: String,
|
|
pub symbols: Vec<String>,
|
|
pub start_date: DateTime<Utc>,
|
|
pub end_date: DateTime<Utc>,
|
|
pub initial_capital: Decimal,
|
|
}
|
|
|
|
/// Mock backtest response
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockBacktestResponse {
|
|
pub backtest_id: Uuid,
|
|
pub status: MockBacktestStatus,
|
|
pub message: String,
|
|
}
|
|
|
|
/// Mock service errors
|
|
#[derive(Debug, Clone, thiserror::Error)]
|
|
pub enum MockServiceError {
|
|
#[error("Order not found: {0}")]
|
|
OrderNotFound(Uuid),
|
|
|
|
#[error("Job not found: {0}")]
|
|
JobNotFound(Uuid),
|
|
|
|
#[error("Backtest not found: {0}")]
|
|
BacktestNotFound(Uuid),
|
|
|
|
#[error("Invalid operation: {0}")]
|
|
InvalidOperation(String),
|
|
|
|
#[error("Simulated failure")]
|
|
SimulatedFailure,
|
|
|
|
#[error("Service unavailable")]
|
|
ServiceUnavailable,
|
|
}
|
|
|
|
// =============================================================================
|
|
// SERVICE FACTORY
|
|
// =============================================================================
|
|
|
|
/// Factory for creating mock services
|
|
#[derive(Debug)]
|
|
pub struct MockServiceFactory {
|
|
config: TestConfig,
|
|
}
|
|
|
|
impl MockServiceFactory {
|
|
pub fn new(config: TestConfig) -> Self {
|
|
Self { config }
|
|
}
|
|
|
|
pub fn create_trading_service(&self) -> MockTradingService {
|
|
MockTradingService::new(self.config.clone())
|
|
}
|
|
|
|
pub fn create_ml_training_service(&self) -> MockMLTrainingService {
|
|
MockMLTrainingService::new(self.config.clone())
|
|
}
|
|
|
|
pub fn create_backtesting_service(&self) -> MockBacktestingService {
|
|
MockBacktestingService::new(self.config.clone())
|
|
}
|
|
|
|
/// Create all services
|
|
pub fn create_all_services(&self) -> (MockTradingService, MockMLTrainingService, MockBacktestingService) {
|
|
(
|
|
self.create_trading_service(),
|
|
self.create_ml_training_service(),
|
|
self.create_backtesting_service(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::fixtures::TEST_EQUITY_1;
|
|
|
|
#[tokio::test]
|
|
async fn test_mock_trading_service() {
|
|
let config = TestConfig::for_unit_tests();
|
|
let service = MockTradingService::new(config);
|
|
|
|
let request = MockOrderRequest {
|
|
symbol: TEST_EQUITY_1.to_string(),
|
|
side: MockOrderSide::Buy,
|
|
quantity: Decimal::from(100),
|
|
price: Decimal::from(150),
|
|
order_type: MockOrderType::Limit,
|
|
};
|
|
|
|
let response = service.submit_order(request).await.unwrap();
|
|
assert_eq!(response.status, MockOrderStatus::Pending);
|
|
|
|
// Wait for processing
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
let order = service.get_order_status(response.order_id).await.unwrap();
|
|
assert_eq!(order.status, MockOrderStatus::Filled);
|
|
assert_eq!(order.filled_quantity, order.quantity);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_mock_ml_training_service() {
|
|
let config = TestConfig::for_unit_tests();
|
|
let service = MockMLTrainingService::new(config);
|
|
|
|
let request = MockTrainingRequest {
|
|
model_name: "test_model".to_string(),
|
|
model_type: MockModelType::Transformer,
|
|
data_path: "/data/test.csv".to_string(),
|
|
hyperparameters: HashMap::new(),
|
|
};
|
|
|
|
let response = service.start_training(request).await.unwrap();
|
|
assert_eq!(response.status, MockTrainingStatus::Running);
|
|
|
|
// Wait for training to complete
|
|
tokio::time::sleep(Duration::from_secs(6)).await;
|
|
|
|
let job = service.get_training_status(response.job_id).await.unwrap();
|
|
assert_eq!(job.status, MockTrainingStatus::Completed);
|
|
assert_eq!(job.progress, 1.0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_mock_backtesting_service() {
|
|
let config = TestConfig::for_unit_tests();
|
|
let service = MockBacktestingService::new(config);
|
|
|
|
let request = MockBacktestRequest {
|
|
strategy_name: "test_strategy".to_string(),
|
|
symbols: vec![TEST_EQUITY_1.to_string()],
|
|
start_date: Utc::now() - chrono::Duration::days(30),
|
|
end_date: Utc::now(),
|
|
initial_capital: Decimal::from(100000),
|
|
};
|
|
|
|
let response = service.start_backtest(request).await.unwrap();
|
|
assert_eq!(response.status, MockBacktestStatus::Running);
|
|
|
|
// Wait for backtest to complete
|
|
tokio::time::sleep(Duration::from_secs(4)).await;
|
|
|
|
let backtest = service.get_backtest_status(response.backtest_id).await.unwrap();
|
|
assert_eq!(backtest.status, MockBacktestStatus::Completed);
|
|
assert!(backtest.results.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_service_factory() {
|
|
let config = TestConfig::for_unit_tests();
|
|
let factory = MockServiceFactory::new(config);
|
|
|
|
let (trading, ml, backtesting) = factory.create_all_services();
|
|
|
|
// Test that all services are created
|
|
assert_eq!(trading.latency_ms, 0);
|
|
assert_eq!(ml.latency_ms, 0);
|
|
assert_eq!(backtesting.latency_ms, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_failure_simulation() {
|
|
let mut config = TestConfig::for_unit_tests();
|
|
config.services.mock_failure_rate = 1.0; // 100% failure rate
|
|
|
|
let service = MockTradingService::new(config);
|
|
|
|
let request = MockOrderRequest {
|
|
symbol: TEST_EQUITY_1.to_string(),
|
|
side: MockOrderSide::Buy,
|
|
quantity: Decimal::from(100),
|
|
price: Decimal::from(150),
|
|
order_type: MockOrderType::Limit,
|
|
};
|
|
|
|
let result = service.submit_order(request).await;
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), MockServiceError::SimulatedFailure));
|
|
}
|
|
} |