feat(ml): WAVE 29 DQN Codebase Cleanup & Refactoring Campaign
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>
This commit is contained in:
138
tests/test_common/src/assertions/custom_asserts.rs
Normal file
138
tests/test_common/src/assertions/custom_asserts.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
//! Custom assertions for domain-specific testing
|
||||
|
||||
/// Assert that a value is within a percentage tolerance
|
||||
#[macro_export]
|
||||
macro_rules! assert_approx_eq {
|
||||
($left:expr, $right:expr, $tolerance:expr) => {
|
||||
let diff = ($left - $right).abs();
|
||||
let threshold = $right.abs() * $tolerance;
|
||||
assert!(
|
||||
diff <= threshold,
|
||||
"assertion failed: `{} ≈ {}` (tolerance: {}%)\n left: {}\n right: {}\n diff: {} > {}",
|
||||
stringify!($left),
|
||||
stringify!($right),
|
||||
$tolerance * 100.0,
|
||||
$left,
|
||||
$right,
|
||||
diff,
|
||||
threshold
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/// Assert that a value is within an absolute tolerance
|
||||
#[macro_export]
|
||||
macro_rules! assert_within {
|
||||
($value:expr, $target:expr, $tolerance:expr) => {
|
||||
let diff = ($value - $target).abs();
|
||||
assert!(
|
||||
diff <= $tolerance,
|
||||
"assertion failed: `{} within {} of {}`\n value: {}\n target: {}\n diff: {}",
|
||||
stringify!($value),
|
||||
$tolerance,
|
||||
stringify!($target),
|
||||
$value,
|
||||
$target,
|
||||
diff
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/// Assert that VaR exceedances are within expected range
|
||||
pub fn assert_var_exceedances(
|
||||
returns: &[f64],
|
||||
var: f64,
|
||||
confidence: f64,
|
||||
max_deviation: f64,
|
||||
) {
|
||||
let exceedances = returns.iter().filter(|&&r| r.abs() > var).count();
|
||||
let expected_exceedances = (returns.len() as f64 * (1.0 - confidence)) as usize;
|
||||
let max_allowed = (expected_exceedances as f64 * (1.0 + max_deviation)) as usize;
|
||||
|
||||
assert!(
|
||||
exceedances <= max_allowed,
|
||||
"VaR exceedances {} exceed maximum {} (expected: {}, confidence: {}, tolerance: {}%)",
|
||||
exceedances,
|
||||
max_allowed,
|
||||
expected_exceedances,
|
||||
confidence,
|
||||
max_deviation * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
/// Assert that OHLC relationships are valid
|
||||
pub fn assert_ohlc_valid(open: f64, high: f64, low: f64, close: f64) {
|
||||
assert!(
|
||||
high >= open,
|
||||
"High {} must be >= Open {}",
|
||||
high,
|
||||
open
|
||||
);
|
||||
assert!(
|
||||
high >= close,
|
||||
"High {} must be >= Close {}",
|
||||
high,
|
||||
close
|
||||
);
|
||||
assert!(
|
||||
low <= open,
|
||||
"Low {} must be <= Open {}",
|
||||
low,
|
||||
open
|
||||
);
|
||||
assert!(
|
||||
low <= close,
|
||||
"Low {} must be <= Close {}",
|
||||
low,
|
||||
close
|
||||
);
|
||||
}
|
||||
|
||||
/// Assert that a position's PnL is correct
|
||||
pub fn assert_pnl(
|
||||
entry_price: f64,
|
||||
current_price: f64,
|
||||
quantity: f64,
|
||||
expected_pnl: f64,
|
||||
tolerance: f64,
|
||||
) {
|
||||
let actual_pnl = (current_price - entry_price) * quantity;
|
||||
assert_within!(actual_pnl, expected_pnl, tolerance);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_assert_approx_eq() {
|
||||
assert_approx_eq!(100.0, 101.0, 0.02); // 2% tolerance
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_assert_approx_eq_fails() {
|
||||
assert_approx_eq!(100.0, 105.0, 0.02); // Should fail
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_within() {
|
||||
assert_within!(100.5, 100.0, 0.6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_ohlc_valid() {
|
||||
assert_ohlc_valid(100.0, 102.0, 98.0, 101.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_assert_ohlc_invalid() {
|
||||
assert_ohlc_valid(100.0, 98.0, 99.0, 101.0); // High < Open
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_pnl() {
|
||||
assert_pnl(100.0, 110.0, 50.0, 500.0, 0.01);
|
||||
}
|
||||
}
|
||||
5
tests/test_common/src/assertions/mod.rs
Normal file
5
tests/test_common/src/assertions/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Domain-specific assertions for testing
|
||||
|
||||
pub mod custom_asserts;
|
||||
|
||||
pub use custom_asserts::*;
|
||||
154
tests/test_common/src/builders/bar_builder.rs
Normal file
154
tests/test_common/src/builders/bar_builder.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
//! OHLCV Bar builder for testing
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use ml::real_data_loader::OHLCVBar;
|
||||
|
||||
/// Builder for creating OHLCV bars
|
||||
pub struct BarBuilder {
|
||||
timestamp: DateTime<Utc>,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
}
|
||||
|
||||
impl BarBuilder {
|
||||
pub fn new() -> Self {
|
||||
let price = 100.0;
|
||||
Self {
|
||||
timestamp: Utc::now(),
|
||||
open: price,
|
||||
high: price * 1.01,
|
||||
low: price * 0.99,
|
||||
close: price,
|
||||
volume: 1000.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
|
||||
self.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn price(mut self, price: f64) -> Self {
|
||||
self.open = price;
|
||||
self.close = price;
|
||||
self.high = price * 1.01;
|
||||
self.low = price * 0.99;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn open(mut self, open: f64) -> Self {
|
||||
self.open = open;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn high(mut self, high: f64) -> Self {
|
||||
self.high = high;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn low(mut self, low: f64) -> Self {
|
||||
self.low = low;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn close(mut self, close: f64) -> Self {
|
||||
self.close = close;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn volume(mut self, volume: f64) -> Self {
|
||||
self.volume = volume;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bullish(mut self) -> Self {
|
||||
self.close = self.open * 1.05;
|
||||
self.high = self.close * 1.01;
|
||||
self.low = self.open * 0.99;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bearish(mut self) -> Self {
|
||||
self.close = self.open * 0.95;
|
||||
self.high = self.open * 1.01;
|
||||
self.low = self.close * 0.99;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> OHLCVBar {
|
||||
OHLCVBar {
|
||||
timestamp: self.timestamp,
|
||||
open: self.open,
|
||||
high: self.high,
|
||||
low: self.low,
|
||||
close: self.close,
|
||||
volume: self.volume,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a series of bars with specified spacing
|
||||
pub fn build_series(self, count: usize, interval_minutes: i64) -> Vec<OHLCVBar> {
|
||||
let mut bars = Vec::with_capacity(count);
|
||||
let mut current = self.build();
|
||||
|
||||
for _ in 0..count {
|
||||
bars.push(current.clone());
|
||||
current.timestamp = current.timestamp + Duration::minutes(interval_minutes);
|
||||
// Add small random walk
|
||||
current.open = current.close;
|
||||
current.close = current.close * (1.0 + (rand::random::<f64>() - 0.5) * 0.02);
|
||||
current.high = current.close.max(current.open) * 1.005;
|
||||
current.low = current.close.min(current.open) * 0.995;
|
||||
}
|
||||
|
||||
bars
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BarBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bar_builder_defaults() {
|
||||
let bar = BarBuilder::new().build();
|
||||
assert_eq!(bar.open, 100.0);
|
||||
assert!(bar.high >= bar.open);
|
||||
assert!(bar.low <= bar.open);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bullish_bar() {
|
||||
let bar = BarBuilder::new().bullish().build();
|
||||
assert!(bar.close > bar.open);
|
||||
assert_eq!(bar.close / bar.open, 1.05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bearish_bar() {
|
||||
let bar = BarBuilder::new().bearish().build();
|
||||
assert!(bar.close < bar.open);
|
||||
assert_eq!(bar.close / bar.open, 0.95);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_series() {
|
||||
let bars = BarBuilder::new().build_series(10, 5);
|
||||
assert_eq!(bars.len(), 10);
|
||||
|
||||
// Check timestamps are 5 minutes apart
|
||||
for i in 0..bars.len() - 1 {
|
||||
let diff = bars[i + 1].timestamp - bars[i].timestamp;
|
||||
assert_eq!(diff.num_minutes(), 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
tests/test_common/src/builders/mod.rs
Normal file
9
tests/test_common/src/builders/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Builder patterns for test objects
|
||||
|
||||
pub mod bar_builder;
|
||||
pub mod order_builder;
|
||||
pub mod position_builder;
|
||||
|
||||
pub use bar_builder::BarBuilder;
|
||||
pub use order_builder::OrderBuilder;
|
||||
pub use position_builder::PositionBuilder;
|
||||
3
tests/test_common/src/builders/order_builder.rs
Normal file
3
tests/test_common/src/builders/order_builder.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
//! Order builder (re-export from fixtures for convenience)
|
||||
|
||||
pub use crate::fixtures::orders::{MockOrder, MockOrderBuilder as OrderBuilder, Side, OrderType, OrderStatus};
|
||||
149
tests/test_common/src/builders/position_builder.rs
Normal file
149
tests/test_common/src/builders/position_builder.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
//! Position builder for testing
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Mock position for testing
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockPosition {
|
||||
pub symbol: String,
|
||||
pub quantity: f64,
|
||||
pub entry_price: f64,
|
||||
pub current_price: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl MockPosition {
|
||||
pub fn pnl(&self) -> f64 {
|
||||
(self.current_price - self.entry_price) * self.quantity
|
||||
}
|
||||
|
||||
pub fn pnl_percent(&self) -> f64 {
|
||||
((self.current_price - self.entry_price) / self.entry_price) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating mock positions
|
||||
pub struct PositionBuilder {
|
||||
symbol: String,
|
||||
quantity: f64,
|
||||
entry_price: f64,
|
||||
current_price: f64,
|
||||
timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl PositionBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
symbol: "AAPL".to_string(),
|
||||
quantity: 100.0,
|
||||
entry_price: 150.0,
|
||||
current_price: 150.0,
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn symbol(mut self, symbol: &str) -> Self {
|
||||
self.symbol = symbol.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn long(mut self, quantity: f64) -> Self {
|
||||
self.quantity = quantity.abs();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn short(mut self, quantity: f64) -> Self {
|
||||
self.quantity = -quantity.abs();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn entry_price(mut self, price: f64) -> Self {
|
||||
self.entry_price = price;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn current_price(mut self, price: f64) -> Self {
|
||||
self.current_price = price;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn profitable(mut self, percent: f64) -> Self {
|
||||
self.current_price = self.entry_price * (1.0 + percent / 100.0);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn losing(mut self, percent: f64) -> Self {
|
||||
self.current_price = self.entry_price * (1.0 - percent / 100.0);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MockPosition {
|
||||
MockPosition {
|
||||
symbol: self.symbol,
|
||||
quantity: self.quantity,
|
||||
entry_price: self.entry_price,
|
||||
current_price: self.current_price,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PositionBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_position_builder() {
|
||||
let pos = PositionBuilder::new()
|
||||
.symbol("TSLA")
|
||||
.long(50.0)
|
||||
.entry_price(200.0)
|
||||
.current_price(220.0)
|
||||
.build();
|
||||
|
||||
assert_eq!(pos.symbol, "TSLA");
|
||||
assert_eq!(pos.quantity, 50.0);
|
||||
assert_eq!(pos.pnl(), 1000.0); // (220 - 200) * 50
|
||||
assert_eq!(pos.pnl_percent(), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_position() {
|
||||
let pos = PositionBuilder::new()
|
||||
.short(100.0)
|
||||
.entry_price(150.0)
|
||||
.current_price(140.0)
|
||||
.build();
|
||||
|
||||
assert!(pos.quantity < 0.0);
|
||||
assert_eq!(pos.pnl(), -1000.0); // (140 - 150) * -100
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_profitable_position() {
|
||||
let pos = PositionBuilder::new()
|
||||
.entry_price(100.0)
|
||||
.profitable(10.0)
|
||||
.build();
|
||||
|
||||
assert_eq!(pos.current_price, 110.0);
|
||||
assert_eq!(pos.pnl_percent(), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_losing_position() {
|
||||
let pos = PositionBuilder::new()
|
||||
.entry_price(100.0)
|
||||
.losing(5.0)
|
||||
.build();
|
||||
|
||||
assert_eq!(pos.current_price, 95.0);
|
||||
assert_eq!(pos.pnl_percent(), -5.0);
|
||||
}
|
||||
}
|
||||
131
tests/test_common/src/fixtures/config.rs
Normal file
131
tests/test_common/src/fixtures/config.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! Configuration test fixtures
|
||||
//!
|
||||
//! Provides test configurations for various components.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test configuration builder
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestConfig {
|
||||
pub database_url: String,
|
||||
pub storage_path: PathBuf,
|
||||
pub max_connections: u32,
|
||||
pub timeout_seconds: u64,
|
||||
}
|
||||
|
||||
impl TestConfig {
|
||||
/// Create default test configuration
|
||||
pub fn default_test() -> Self {
|
||||
Self {
|
||||
database_url: "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt"
|
||||
.to_string(),
|
||||
storage_path: PathBuf::from("/tmp/foxhunt_test"),
|
||||
max_connections: 5,
|
||||
timeout_seconds: 30,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create configuration with temporary directory
|
||||
pub fn with_temp_dir(temp_dir: &TempDir) -> Self {
|
||||
Self {
|
||||
database_url: "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt"
|
||||
.to_string(),
|
||||
storage_path: temp_dir.path().to_path_buf(),
|
||||
max_connections: 5,
|
||||
timeout_seconds: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ML training test configuration
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MLTestConfig {
|
||||
pub batch_size: usize,
|
||||
pub learning_rate: f64,
|
||||
pub epochs: usize,
|
||||
pub checkpoint_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl MLTestConfig {
|
||||
pub fn default_test() -> Self {
|
||||
Self {
|
||||
batch_size: 32,
|
||||
learning_rate: 0.001,
|
||||
epochs: 10,
|
||||
checkpoint_dir: PathBuf::from("/tmp/foxhunt_checkpoints"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_temp_dir(temp_dir: &TempDir) -> Self {
|
||||
Self {
|
||||
batch_size: 32,
|
||||
learning_rate: 0.001,
|
||||
epochs: 10,
|
||||
checkpoint_dir: temp_dir.path().to_path_buf(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Risk engine test configuration
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RiskTestConfig {
|
||||
pub var_confidence: f64,
|
||||
pub max_position_size: f64,
|
||||
pub circuit_breaker_threshold: f64,
|
||||
}
|
||||
|
||||
impl RiskTestConfig {
|
||||
pub fn default_test() -> Self {
|
||||
Self {
|
||||
var_confidence: 0.99,
|
||||
max_position_size: 100000.0,
|
||||
circuit_breaker_threshold: 0.10,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn conservative() -> Self {
|
||||
Self {
|
||||
var_confidence: 0.99,
|
||||
max_position_size: 50000.0,
|
||||
circuit_breaker_threshold: 0.05,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aggressive() -> Self {
|
||||
Self {
|
||||
var_confidence: 0.95,
|
||||
max_position_size: 200000.0,
|
||||
circuit_breaker_threshold: 0.15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = TestConfig::default_test();
|
||||
assert_eq!(config.max_connections, 5);
|
||||
assert_eq!(config.timeout_seconds, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ml_config() {
|
||||
let config = MLTestConfig::default_test();
|
||||
assert_eq!(config.batch_size, 32);
|
||||
assert_eq!(config.learning_rate, 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_risk_configs() {
|
||||
let conservative = RiskTestConfig::conservative();
|
||||
let aggressive = RiskTestConfig::aggressive();
|
||||
|
||||
assert!(conservative.max_position_size < aggressive.max_position_size);
|
||||
assert!(conservative.circuit_breaker_threshold < aggressive.circuit_breaker_threshold);
|
||||
}
|
||||
}
|
||||
168
tests/test_common/src/fixtures/database.rs
Normal file
168
tests/test_common/src/fixtures/database.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! Database test fixtures
|
||||
//!
|
||||
//! Provides shared database setup/teardown utilities used across 39+ test files.
|
||||
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use sqlx::{postgres::PgPoolOptions, PgPool};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
static DB_POOL: OnceCell<Arc<Mutex<PgPool>>> = OnceCell::new();
|
||||
|
||||
/// Test database wrapper with automatic setup and cleanup
|
||||
pub struct TestDb {
|
||||
pool: PgPool,
|
||||
schema_name: String,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
/// Create a new test database instance
|
||||
///
|
||||
/// Uses the DATABASE_URL environment variable or falls back to default.
|
||||
/// Creates an isolated schema for test isolation.
|
||||
pub async fn new() -> Self {
|
||||
let pool = get_test_pool().await;
|
||||
let schema_name = format!("test_{}", uuid::Uuid::new_v4().to_string().replace('-', "_"));
|
||||
|
||||
// Create isolated schema
|
||||
sqlx::query(&format!("CREATE SCHEMA {}", schema_name))
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to create test schema");
|
||||
|
||||
Self {
|
||||
pool,
|
||||
schema_name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new test database with migrations applied
|
||||
pub async fn with_migrations() -> Self {
|
||||
let db = Self::new().await;
|
||||
|
||||
// Set search path to test schema
|
||||
sqlx::query(&format!("SET search_path TO {}", db.schema_name))
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.expect("Failed to set search path");
|
||||
|
||||
// Run migrations
|
||||
sqlx::migrate!("../../migrations")
|
||||
.run(&db.pool)
|
||||
.await
|
||||
.expect("Failed to run migrations");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
/// Get a reference to the connection pool
|
||||
pub fn pool(&self) -> &PgPool {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
/// Execute a query in the test schema
|
||||
pub async fn execute(&self, sql: &str) -> Result<sqlx::postgres::PgQueryResult> {
|
||||
sqlx::query(&format!("SET search_path TO {}; {}", self.schema_name, sql))
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Begin a transaction for test isolation
|
||||
pub async fn begin(&self) -> Result<sqlx::Transaction<'_, sqlx::Postgres>> {
|
||||
self.pool.begin().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDb {
|
||||
fn drop(&mut self) {
|
||||
// Clean up test schema
|
||||
let pool = self.pool.clone();
|
||||
let schema_name = self.schema_name.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = sqlx::query(&format!("DROP SCHEMA IF EXISTS {} CASCADE", schema_name))
|
||||
.execute(&pool)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or create the shared test database pool
|
||||
///
|
||||
/// Pattern found in 39 test files:
|
||||
/// - services/ml_training_service/tests/checkpoint_manager_tests.rs
|
||||
/// - services/ml_training_service/tests/job_tracker_test.rs
|
||||
/// - database/src/test_helpers.rs
|
||||
/// - And 36+ more
|
||||
async fn get_test_pool() -> PgPool {
|
||||
let pool = DB_POOL.get_or_init(|| {
|
||||
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
||||
"postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string()
|
||||
});
|
||||
|
||||
Arc::new(Mutex::new(
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.expect("Failed to create test pool")
|
||||
})
|
||||
})
|
||||
))
|
||||
});
|
||||
|
||||
pool.lock().await.clone()
|
||||
}
|
||||
|
||||
/// Setup test data in the database
|
||||
///
|
||||
/// Pattern found in data_loader_integration.rs
|
||||
pub async fn setup_test_data(pool: &PgPool, symbol_prefix: &str) -> Result<()> {
|
||||
// Clean existing test data
|
||||
sqlx::query(&format!(
|
||||
"DELETE FROM market_events WHERE symbol LIKE '{}%'",
|
||||
symbol_prefix
|
||||
))
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(&format!(
|
||||
"DELETE FROM trade_executions WHERE symbol LIKE '{}%'",
|
||||
symbol_prefix
|
||||
))
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_db_creation() {
|
||||
let db = TestDb::new().await;
|
||||
assert!(db.pool().is_closed() == false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_db_with_migrations() {
|
||||
let db = TestDb::with_migrations().await;
|
||||
let result = db.execute("SELECT 1").await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_schema_isolation() {
|
||||
let db1 = TestDb::new().await;
|
||||
let db2 = TestDb::new().await;
|
||||
|
||||
// Different schemas should be isolated
|
||||
assert_ne!(db1.schema_name, db2.schema_name);
|
||||
}
|
||||
}
|
||||
203
tests/test_common/src/fixtures/market_data.rs
Normal file
203
tests/test_common/src/fixtures/market_data.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
//! Market data test fixtures
|
||||
//!
|
||||
//! Provides mock OHLCV bars, ticks, and market data generators.
|
||||
//! Consolidates patterns from 15+ test files with `create_mock_bars()` functions.
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use ml::real_data_loader::OHLCVBar;
|
||||
use rand::Rng;
|
||||
|
||||
/// Generate OHLCV bars with realistic price movements
|
||||
///
|
||||
/// Pattern found in:
|
||||
/// - ml/tests/feature_cache_tests.rs
|
||||
/// - ml/tests/dqn_feature_cache_test.rs
|
||||
/// - trading_engine/src/features/mod.rs
|
||||
pub fn generate_ohlcv_bars(count: usize, start_price: f64) -> Vec<OHLCVBar> {
|
||||
let mut bars = Vec::with_capacity(count);
|
||||
let mut current_price = start_price;
|
||||
let base_time = Utc::now() - Duration::minutes((count * 5) as i64);
|
||||
|
||||
for i in 0..count {
|
||||
// Generate realistic price movements
|
||||
let change = ((i as f64 / 100.0).sin() * 0.02) + (rand::thread_rng().gen::<f64>() - 0.5) * 0.01;
|
||||
current_price *= 1.0 + change;
|
||||
|
||||
let high = current_price * (1.0 + rand::thread_rng().gen::<f64>() * 0.005);
|
||||
let low = current_price * (1.0 - rand::thread_rng().gen::<f64>() * 0.005);
|
||||
let close = current_price * (1.0 + (rand::thread_rng().gen::<f64>() - 0.5) * 0.003);
|
||||
|
||||
bars.push(OHLCVBar {
|
||||
timestamp: base_time + Duration::minutes((i * 5) as i64),
|
||||
open: current_price,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume: 1000.0 + rand::thread_rng().gen::<f64>() * 5000.0,
|
||||
});
|
||||
|
||||
current_price = close;
|
||||
}
|
||||
|
||||
bars
|
||||
}
|
||||
|
||||
/// Generate random walk price series
|
||||
///
|
||||
/// Used for Monte Carlo simulations and VaR testing.
|
||||
pub fn generate_random_walk(count: usize, volatility: f64) -> Vec<f64> {
|
||||
let mut prices = Vec::with_capacity(count);
|
||||
let mut current_price = 100.0;
|
||||
|
||||
for _ in 0..count {
|
||||
let change = (rand::thread_rng().gen::<f64>() - 0.5) * volatility;
|
||||
current_price *= 1.0 + change;
|
||||
prices.push(current_price);
|
||||
}
|
||||
|
||||
prices
|
||||
}
|
||||
|
||||
/// Generate crisis scenario returns (2008 Financial Crisis pattern)
|
||||
///
|
||||
/// Pattern from risk/tests/risk_comprehensive_tests.rs
|
||||
pub fn generate_crisis_returns() -> Vec<f64> {
|
||||
vec![
|
||||
-0.08, -0.10, -0.07, -0.12, -0.09,
|
||||
-0.06, -0.15, -0.11, -0.08, -0.13,
|
||||
-0.05, -0.09, -0.14, -0.07, -0.10,
|
||||
-0.12, -0.08, -0.06, -0.11, -0.09,
|
||||
]
|
||||
}
|
||||
|
||||
/// Generate COVID crash returns (March 2020 pattern)
|
||||
///
|
||||
/// Pattern from risk/tests/risk_comprehensive_tests.rs
|
||||
pub fn generate_covid_crash_returns() -> Vec<f64> {
|
||||
vec![
|
||||
-0.12, -0.09, -0.13, -0.08, -0.11,
|
||||
-0.06, -0.10, -0.07, -0.09, -0.14,
|
||||
-0.05, -0.08, -0.12, -0.09, -0.11,
|
||||
-0.13, -0.07, -0.10, -0.06, -0.09,
|
||||
]
|
||||
}
|
||||
|
||||
/// Generate bull market returns
|
||||
pub fn generate_bull_market_returns() -> Vec<f64> {
|
||||
vec![
|
||||
0.05, 0.03, 0.07, 0.04, 0.06,
|
||||
0.02, 0.08, 0.05, 0.04, 0.06,
|
||||
0.03, 0.07, 0.05, 0.04, 0.06,
|
||||
0.08, 0.03, 0.05, 0.07, 0.04,
|
||||
]
|
||||
}
|
||||
|
||||
/// Generate mock market ticks
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockTick {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub price: f64,
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
pub fn generate_market_ticks(count: usize, start_price: f64) -> Vec<MockTick> {
|
||||
let mut ticks = Vec::with_capacity(count);
|
||||
let mut current_price = start_price;
|
||||
let base_time = Utc::now() - Duration::seconds(count as i64);
|
||||
|
||||
for i in 0..count {
|
||||
current_price += (rand::thread_rng().gen::<f64>() - 0.5) * 0.1;
|
||||
|
||||
ticks.push(MockTick {
|
||||
timestamp: base_time + Duration::seconds(i as i64),
|
||||
price: current_price,
|
||||
volume: 10.0 + (rand::thread_rng().gen::<f64>() * 100.0),
|
||||
});
|
||||
}
|
||||
|
||||
ticks
|
||||
}
|
||||
|
||||
/// Generate order book levels
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OrderBookLevel {
|
||||
pub price: f64,
|
||||
pub size: f64,
|
||||
}
|
||||
|
||||
pub fn generate_order_book(mid_price: f64, levels: usize) -> (Vec<OrderBookLevel>, Vec<OrderBookLevel>) {
|
||||
let mut bids = Vec::with_capacity(levels);
|
||||
let mut asks = Vec::with_capacity(levels);
|
||||
|
||||
for i in 0..levels {
|
||||
let spread = (i as f64 + 1.0) * 0.01;
|
||||
|
||||
bids.push(OrderBookLevel {
|
||||
price: mid_price * (1.0 - spread),
|
||||
size: 1000.0 + (rand::thread_rng().gen::<f64>() * 5000.0),
|
||||
});
|
||||
|
||||
asks.push(OrderBookLevel {
|
||||
price: mid_price * (1.0 + spread),
|
||||
size: 1000.0 + (rand::thread_rng().gen::<f64>() * 5000.0),
|
||||
});
|
||||
}
|
||||
|
||||
(bids, asks)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_generate_ohlcv_bars() {
|
||||
let bars = generate_ohlcv_bars(100, 150.0);
|
||||
assert_eq!(bars.len(), 100);
|
||||
|
||||
// Check OHLC relationships
|
||||
for bar in &bars {
|
||||
assert!(bar.high >= bar.open);
|
||||
assert!(bar.high >= bar.close);
|
||||
assert!(bar.low <= bar.open);
|
||||
assert!(bar.low <= bar.close);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_random_walk() {
|
||||
let prices = generate_random_walk(100, 0.02);
|
||||
assert_eq!(prices.len(), 100);
|
||||
assert!(prices[0] > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crisis_returns() {
|
||||
let returns = generate_crisis_returns();
|
||||
assert_eq!(returns.len(), 20);
|
||||
assert!(returns.iter().all(|&r| r < 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_market_ticks() {
|
||||
let ticks = generate_market_ticks(100, 150.0);
|
||||
assert_eq!(ticks.len(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_order_book() {
|
||||
let (bids, asks) = generate_order_book(150.0, 10);
|
||||
assert_eq!(bids.len(), 10);
|
||||
assert_eq!(asks.len(), 10);
|
||||
|
||||
// Bids should be descending
|
||||
for i in 0..bids.len() - 1 {
|
||||
assert!(bids[i].price > bids[i + 1].price);
|
||||
}
|
||||
|
||||
// Asks should be ascending
|
||||
for i in 0..asks.len() - 1 {
|
||||
assert!(asks[i].price < asks[i + 1].price);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
tests/test_common/src/fixtures/mod.rs
Normal file
9
tests/test_common/src/fixtures/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Test fixtures module
|
||||
//!
|
||||
//! Shared fixtures for database, orders, market data, configs, and network responses.
|
||||
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod market_data;
|
||||
pub mod network;
|
||||
pub mod orders;
|
||||
141
tests/test_common/src/fixtures/network.rs
Normal file
141
tests/test_common/src/fixtures/network.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! Network response test fixtures
|
||||
//!
|
||||
//! Provides mock HTTP and WebSocket responses for testing.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
/// Mock HTTP response builder
|
||||
pub struct MockHttpResponse {
|
||||
status: u16,
|
||||
body: String,
|
||||
headers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl MockHttpResponse {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
status: 200,
|
||||
body: String::new(),
|
||||
headers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: u16) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn json_body(mut self, body: serde_json::Value) -> Self {
|
||||
self.body = body.to_string();
|
||||
self.headers.push((
|
||||
"Content-Type".to_string(),
|
||||
"application/json".to_string(),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, key: &str, value: &str) -> Self {
|
||||
self.headers.push((key.to_string(), value.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> (u16, String, Vec<(String, String)>) {
|
||||
(self.status, self.body, self.headers)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MockHttpResponse {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate mock API responses
|
||||
pub fn mock_market_data_response() -> serde_json::Value {
|
||||
json!({
|
||||
"symbol": "AAPL",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"open": 150.00,
|
||||
"high": 152.50,
|
||||
"low": 149.75,
|
||||
"close": 151.25,
|
||||
"volume": 1000000
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mock_order_response() -> serde_json::Value {
|
||||
json!({
|
||||
"order_id": "ord_123456",
|
||||
"symbol": "AAPL",
|
||||
"quantity": 100,
|
||||
"price": 150.00,
|
||||
"side": "buy",
|
||||
"status": "filled",
|
||||
"filled_quantity": 100,
|
||||
"average_fill_price": 150.00
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mock_error_response(code: u16, message: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// WebSocket message fixtures
|
||||
pub fn mock_websocket_trade_message() -> String {
|
||||
json!({
|
||||
"type": "trade",
|
||||
"symbol": "AAPL",
|
||||
"price": 150.00,
|
||||
"size": 100,
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn mock_websocket_quote_message() -> String {
|
||||
json!({
|
||||
"type": "quote",
|
||||
"symbol": "AAPL",
|
||||
"bid": 149.95,
|
||||
"ask": 150.05,
|
||||
"bid_size": 500,
|
||||
"ask_size": 300,
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_http_response_builder() {
|
||||
let (status, body, headers) = MockHttpResponse::new()
|
||||
.status(201)
|
||||
.json_body(json!({"success": true}))
|
||||
.header("X-Custom", "value")
|
||||
.build();
|
||||
|
||||
assert_eq!(status, 201);
|
||||
assert!(body.contains("success"));
|
||||
assert_eq!(headers.len(), 2); // Content-Type + X-Custom
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_responses() {
|
||||
let market_data = mock_market_data_response();
|
||||
assert_eq!(market_data["symbol"], "AAPL");
|
||||
|
||||
let order = mock_order_response();
|
||||
assert_eq!(order["status"], "filled");
|
||||
|
||||
let error = mock_error_response(404, "Not found");
|
||||
assert_eq!(error["error"]["code"], 404);
|
||||
}
|
||||
}
|
||||
257
tests/test_common/src/fixtures/orders.rs
Normal file
257
tests/test_common/src/fixtures/orders.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
//! Order test fixtures
|
||||
//!
|
||||
//! Provides mock order and trade builders used across 22+ test files.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Mock order builder for testing
|
||||
///
|
||||
/// Pattern found in 22+ test files with `create_mock_order()` functions.
|
||||
pub struct MockOrderBuilder {
|
||||
symbol: String,
|
||||
quantity: f64,
|
||||
price: Option<f64>,
|
||||
side: Side,
|
||||
order_type: OrderType,
|
||||
status: OrderStatus,
|
||||
timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Side {
|
||||
Buy,
|
||||
Sell,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum OrderType {
|
||||
Market,
|
||||
Limit,
|
||||
Stop,
|
||||
StopLimit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum OrderStatus {
|
||||
Pending,
|
||||
PartiallyFilled,
|
||||
Filled,
|
||||
Cancelled,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockOrder {
|
||||
pub id: String,
|
||||
pub symbol: String,
|
||||
pub quantity: f64,
|
||||
pub price: Option<f64>,
|
||||
pub side: Side,
|
||||
pub order_type: OrderType,
|
||||
pub status: OrderStatus,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub filled_quantity: f64,
|
||||
pub average_fill_price: Option<f64>,
|
||||
}
|
||||
|
||||
impl MockOrderBuilder {
|
||||
/// Create a new order builder with defaults
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
symbol: "AAPL".to_string(),
|
||||
quantity: 100.0,
|
||||
price: None,
|
||||
side: Side::Buy,
|
||||
order_type: OrderType::Market,
|
||||
status: OrderStatus::Pending,
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the symbol
|
||||
pub fn symbol(mut self, symbol: &str) -> Self {
|
||||
self.symbol = symbol.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set buy side
|
||||
pub fn buy(mut self) -> Self {
|
||||
self.side = Side::Buy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set sell side
|
||||
pub fn sell(mut self) -> Self {
|
||||
self.side = Side::Sell;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set quantity
|
||||
pub fn quantity(mut self, quantity: f64) -> Self {
|
||||
self.quantity = quantity;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set limit price
|
||||
pub fn limit_price(mut self, price: f64) -> Self {
|
||||
self.price = Some(price);
|
||||
self.order_type = OrderType::Limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set as market order
|
||||
pub fn market(mut self) -> Self {
|
||||
self.order_type = OrderType::Market;
|
||||
self.price = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set as filled
|
||||
pub fn filled(mut self) -> Self {
|
||||
self.status = OrderStatus::Filled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the mock order
|
||||
pub fn build(self) -> MockOrder {
|
||||
let is_filled = matches!(self.status, OrderStatus::Filled);
|
||||
MockOrder {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
symbol: self.symbol,
|
||||
quantity: self.quantity,
|
||||
price: self.price,
|
||||
side: self.side,
|
||||
order_type: self.order_type,
|
||||
status: self.status,
|
||||
timestamp: self.timestamp,
|
||||
filled_quantity: if is_filled { self.quantity } else { 0.0 },
|
||||
average_fill_price: self.price,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MockOrderBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock trade builder
|
||||
pub struct MockTradeBuilder {
|
||||
symbol: String,
|
||||
price: f64,
|
||||
quantity: f64,
|
||||
side: Side,
|
||||
timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockTrade {
|
||||
pub id: String,
|
||||
pub symbol: String,
|
||||
pub price: f64,
|
||||
pub quantity: f64,
|
||||
pub side: Side,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl MockTradeBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
symbol: "AAPL".to_string(),
|
||||
price: 150.0,
|
||||
quantity: 100.0,
|
||||
side: Side::Buy,
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn symbol(mut self, symbol: &str) -> Self {
|
||||
self.symbol = symbol.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn price(mut self, price: f64) -> Self {
|
||||
self.price = price;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn quantity(mut self, quantity: f64) -> Self {
|
||||
self.quantity = quantity;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn buy(mut self) -> Self {
|
||||
self.side = Side::Buy;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sell(mut self) -> Self {
|
||||
self.side = Side::Sell;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MockTrade {
|
||||
MockTrade {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
symbol: self.symbol,
|
||||
price: self.price,
|
||||
quantity: self.quantity,
|
||||
side: self.side,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MockTradeBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_order_builder_defaults() {
|
||||
let order = MockOrderBuilder::new().build();
|
||||
assert_eq!(order.symbol, "AAPL");
|
||||
assert_eq!(order.quantity, 100.0);
|
||||
assert!(matches!(order.side, Side::Buy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_order_builder_customization() {
|
||||
let order = MockOrderBuilder::new()
|
||||
.symbol("TSLA")
|
||||
.sell()
|
||||
.quantity(50.0)
|
||||
.limit_price(200.0)
|
||||
.filled()
|
||||
.build();
|
||||
|
||||
assert_eq!(order.symbol, "TSLA");
|
||||
assert_eq!(order.quantity, 50.0);
|
||||
assert!(matches!(order.side, Side::Sell));
|
||||
assert_eq!(order.price, Some(200.0));
|
||||
assert!(matches!(order.status, OrderStatus::Filled));
|
||||
assert_eq!(order.filled_quantity, 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_builder() {
|
||||
let trade = MockTradeBuilder::new()
|
||||
.symbol("NVDA")
|
||||
.price(450.0)
|
||||
.quantity(25.0)
|
||||
.sell()
|
||||
.build();
|
||||
|
||||
assert_eq!(trade.symbol, "NVDA");
|
||||
assert_eq!(trade.price, 450.0);
|
||||
assert_eq!(trade.quantity, 25.0);
|
||||
assert!(matches!(trade.side, Side::Sell));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user