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:
jgrusewski
2025-11-27 23:46:13 +01:00
parent 2c1acda2f3
commit 2df1ea92e1
763 changed files with 247870 additions and 1714 deletions

View 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);
}
}

View File

@@ -0,0 +1,5 @@
//! Domain-specific assertions for testing
pub mod custom_asserts;
pub use custom_asserts::*;

View 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);
}
}
}

View 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;

View 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};

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}
}

View 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;

View 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);
}
}

View 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));
}
}