//! Test Data Builders for Foxhunt HFT Trading System //! //! This module provides builder patterns for creating test data objects //! with sensible defaults and fluent configuration APIs. //! //! ## Usage //! //! ```rust //! use tests::fixtures::builders::{PortfolioBuilder, InstrumentBuilder, PositionBuilder}; //! use tests::fixtures::{TEST_EQUITY_1, TEST_PORTFOLIO_1}; //! //! // Build a test portfolio //! let portfolio = PortfolioBuilder::new() //! .with_id(TEST_PORTFOLIO_1) //! .with_name("Test Portfolio") //! .with_base_currency("USD") //! .build(); //! //! // Build a test instrument //! let instrument = InstrumentBuilder::new() //! .with_symbol(TEST_EQUITY_1) //! .with_name("Test Equity 1") //! .equity() //! .build(); //! //! // Build a test position //! let position = PositionBuilder::new() //! .with_portfolio_id(TEST_PORTFOLIO_1) //! .with_symbol(TEST_EQUITY_1) //! .with_quantity(Decimal::from(100)) //! .with_price(Decimal::from(150)) //! .build(); //! ``` use ::rust_decimal::prelude::ToPrimitive; use ::rust_decimal::Decimal; use chrono::{DateTime, Utc}; use serde_json::json; use uuid::Uuid; // Import Position from risk crate to match scenarios.rs usage use common::types::Price; use risk::risk_types::Position; use crate::fixtures::helpers::ToDecimal; // Use local test fixture types defined in mod.rs use super::*; // ============================================================================= // INSTRUMENT BUILDER // ============================================================================= /// Builder for creating test Instrument objects #[derive(Debug, Clone)] pub struct InstrumentBuilder { id: Uuid, symbol: String, isin: Option, cusip: Option, bloomberg_id: Option, reuters_id: Option, name: String, instrument_type: InstrumentType, asset_class: AssetClass, sector: Option, currency: String, exchange: Option, tick_size: Option, lot_size: Option, multiplier: Option, maturity_date: Option>, strike_price: Option, option_type: Option, underlying_symbol: Option, is_active: bool, created_at: DateTime, updated_at: DateTime, metadata: serde_json::Value, } impl Default for InstrumentBuilder { fn default() -> Self { Self::new() } } impl InstrumentBuilder { pub fn new() -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), symbol: TEST_EQUITY_1.to_string(), isin: None, cusip: None, bloomberg_id: None, reuters_id: None, name: "Test Instrument".to_string(), instrument_type: InstrumentType::Equity, asset_class: AssetClass::Equities, sector: Some(MarketSector::Technology), currency: "USD".to_string(), exchange: Some("TEST_EXCHANGE".to_string()), tick_size: Some(Decimal::new(1, 2)), // 0.01 lot_size: Some(Decimal::from(1)), multiplier: Some(Decimal::from(1)), maturity_date: None, strike_price: None, option_type: None, underlying_symbol: None, is_active: true, created_at: now, updated_at: now, metadata: json!({"test_data": true}), } } pub fn with_id(mut self, id: Uuid) -> Self { self.id = id; self } pub fn with_symbol(mut self, symbol: impl Into) -> Self { self.symbol = symbol.into(); self } pub fn with_name(mut self, name: impl Into) -> Self { self.name = name.into(); self } pub fn with_currency(mut self, currency: impl Into) -> Self { self.currency = currency.into(); self } pub fn with_exchange(mut self, exchange: impl Into) -> Self { self.exchange = Some(exchange.into()); self } pub fn with_sector(mut self, sector: MarketSector) -> Self { self.sector = Some(sector); self } pub fn with_tick_size(mut self, tick_size: Decimal) -> Self { self.tick_size = Some(tick_size); self } pub fn with_lot_size(mut self, lot_size: Decimal) -> Self { self.lot_size = Some(lot_size); self } pub fn with_multiplier(mut self, multiplier: Decimal) -> Self { self.multiplier = Some(multiplier); self } pub fn with_maturity_date(mut self, maturity_date: DateTime) -> Self { self.maturity_date = Some(maturity_date); self } pub fn with_strike_price(mut self, strike_price: Decimal) -> Self { self.strike_price = Some(strike_price); self } pub fn with_option_type(mut self, option_type: impl Into) -> Self { self.option_type = Some(option_type.into()); self } pub fn with_underlying_symbol(mut self, underlying_symbol: impl Into) -> Self { self.underlying_symbol = Some(underlying_symbol.into()); self } pub fn inactive(mut self) -> Self { self.is_active = false; self } pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self { self.metadata = metadata; self } // Asset class convenience methods pub fn equity(mut self) -> Self { self.instrument_type = InstrumentType::Equity; self.asset_class = AssetClass::Equities; self.sector = Some(MarketSector::Technology); self } pub fn bond(mut self) -> Self { self.instrument_type = InstrumentType::Bond; self.asset_class = AssetClass::FixedIncome; self.sector = Some(MarketSector::Financials); self } pub fn currency(mut self) -> Self { self.instrument_type = InstrumentType::Currency; self.asset_class = AssetClass::Currencies; self.sector = None; self } pub fn future(mut self) -> Self { self.instrument_type = InstrumentType::Future; self.asset_class = AssetClass::Derivatives; self.sector = None; self } pub fn option(mut self) -> Self { self.instrument_type = InstrumentType::Option; self.asset_class = AssetClass::Derivatives; self.sector = None; self } pub fn commodity(mut self) -> Self { self.instrument_type = InstrumentType::Commodity; self.asset_class = AssetClass::Commodities; self.sector = Some(MarketSector::Materials); self } pub fn crypto(mut self) -> Self { self.instrument_type = InstrumentType::Crypto; self.asset_class = AssetClass::Alternatives; self.sector = None; self } pub fn build(self) -> Instrument { Instrument { id: self.id, symbol: self.symbol, isin: self.isin, cusip: self.cusip, bloomberg_id: self.bloomberg_id, reuters_id: self.reuters_id, name: self.name, instrument_type: self.instrument_type, asset_class: self.asset_class, sector: self.sector, currency: self.currency, exchange: self.exchange, tick_size: self.tick_size, lot_size: self.lot_size, multiplier: self.multiplier, maturity_date: self.maturity_date, strike_price: self.strike_price, option_type: self.option_type, underlying_symbol: self.underlying_symbol, is_active: self.is_active, created_at: self.created_at, updated_at: self.updated_at, metadata: self.metadata, } } } // ============================================================================= // PORTFOLIO BUILDER // ============================================================================= /// Builder for creating test Portfolio objects #[derive(Debug, Clone)] pub struct PortfolioBuilder { id: String, name: String, description: Option, base_currency: String, portfolio_type: String, inception_date: DateTime, manager_id: String, benchmark: Option, risk_budget: Option, var_limit: Option, max_drawdown_limit: Option, is_active: bool, created_at: DateTime, updated_at: DateTime, metadata: serde_json::Value, } impl Default for PortfolioBuilder { fn default() -> Self { Self::new() } } impl PortfolioBuilder { pub fn new() -> Self { let now = Utc::now(); Self { id: TEST_PORTFOLIO_1.to_string(), name: "Test Portfolio".to_string(), description: Some("Test portfolio for automated testing".to_string()), base_currency: "USD".to_string(), portfolio_type: "Test".to_string(), inception_date: now, manager_id: "test_manager".to_string(), benchmark: Some("SPY".to_string()), risk_budget: Some(Decimal::new(15, 2)), // 0.15 (15%) var_limit: Some(Decimal::from(100000)), max_drawdown_limit: Some(Decimal::new(20, 2)), // 0.20 (20%) is_active: true, created_at: now, updated_at: now, metadata: json!({"test_data": true}), } } pub fn with_id(mut self, id: impl Into) -> Self { self.id = id.into(); self } pub fn with_name(mut self, name: impl Into) -> Self { self.name = name.into(); self } pub fn with_description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } pub fn with_base_currency(mut self, currency: impl Into) -> Self { self.base_currency = currency.into(); self } pub fn with_portfolio_type(mut self, portfolio_type: impl Into) -> Self { self.portfolio_type = portfolio_type.into(); self } pub fn with_inception_date(mut self, date: DateTime) -> Self { self.inception_date = date; self } pub fn with_manager_id(mut self, manager_id: impl Into) -> Self { self.manager_id = manager_id.into(); self } pub fn with_benchmark(mut self, benchmark: impl Into) -> Self { self.benchmark = Some(benchmark.into()); self } pub fn with_risk_budget(mut self, risk_budget: Decimal) -> Self { self.risk_budget = Some(risk_budget); self } pub fn with_var_limit(mut self, var_limit: Decimal) -> Self { self.var_limit = Some(var_limit); self } pub fn with_max_drawdown_limit(mut self, limit: Decimal) -> Self { self.max_drawdown_limit = Some(limit); self } pub fn inactive(mut self) -> Self { self.is_active = false; self } pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self { self.metadata = metadata; self } // Portfolio type convenience methods pub fn strategy_portfolio(mut self) -> Self { self.portfolio_type = "Strategy".to_string(); self } pub fn hedge_fund(mut self) -> Self { self.portfolio_type = "Hedge Fund".to_string(); self } pub fn long_only(mut self) -> Self { self.portfolio_type = "Long Only".to_string(); self } pub fn market_neutral(mut self) -> Self { self.portfolio_type = "Market Neutral".to_string(); self } pub fn build(self) -> Portfolio { Portfolio { id: self.id, name: self.name, description: self.description, base_currency: self.base_currency, portfolio_type: self.portfolio_type, inception_date: self.inception_date, manager_id: self.manager_id, benchmark: self.benchmark, risk_budget: self.risk_budget, var_limit: self.var_limit, max_drawdown_limit: self.max_drawdown_limit, is_active: self.is_active, created_at: self.created_at, updated_at: self.updated_at, metadata: self.metadata, } } } // ============================================================================= // POSITION BUILDER // ============================================================================= /// Builder for creating test Position objects (risk::risk_types::Position) #[derive(Debug, Clone)] pub struct PositionBuilder { symbol: String, quantity: f64, market_price: f64, market_value: f64, average_cost: f64, average_price: Price, unrealized_pnl: f64, realized_pnl: f64, last_updated: i64, // Additional builder fields for convenience (not in final Position) _portfolio_id: Option, _weight: Option, _beta: Option, _duration: Option, } impl Default for PositionBuilder { fn default() -> Self { Self::new() } } impl PositionBuilder { pub fn new() -> Self { let quantity = 100.0; let price = 100.0; let market_value = quantity * price; Self { symbol: TEST_EQUITY_1.to_string(), quantity, market_price: price, market_value, average_cost: price, average_price: Price::from_f64(price).unwrap_or_else(|_| Price::new(0.0).unwrap()), unrealized_pnl: 0.0, realized_pnl: 0.0, last_updated: Utc::now().timestamp(), _portfolio_id: Some(TEST_PORTFOLIO_1.to_string()), _weight: Some(0.05), // 5% _beta: Some(1.2), _duration: None, } } pub fn with_portfolio_id(mut self, portfolio_id: impl Into) -> Self { self._portfolio_id = Some(portfolio_id.into()); self } pub fn with_symbol(mut self, symbol: impl Into) -> Self { self.symbol = symbol.into(); self } pub fn with_quantity(mut self, quantity: Decimal) -> Self { self.quantity = quantity.to_f64().unwrap_or(0.0); // Recalculate market value self.market_value = self.quantity * self.market_price; self.unrealized_pnl = (self.market_price - self.average_cost) * self.quantity; self } pub fn with_average_price(mut self, price: Decimal) -> Self { let price_f64 = price.to_f64().unwrap_or(0.0); self.average_cost = price_f64; self.average_price = Price::from_f64(price_f64).unwrap_or_else(|_| Price::new(0.0).unwrap()); // Recalculate unrealized PnL self.unrealized_pnl = (self.market_price - price_f64) * self.quantity; self } pub fn with_market_price(mut self, price: Decimal) -> Self { let price_f64 = price.to_f64().unwrap_or(0.0); self.market_price = price_f64; // Recalculate market value and unrealized PnL self.market_value = self.quantity * price_f64; self.unrealized_pnl = (price_f64 - self.average_cost) * self.quantity; self } pub fn with_weight(mut self, weight: Decimal) -> Self { self._weight = Some(weight.to_f64().unwrap_or(0.0)); self } pub fn with_beta(mut self, beta: Decimal) -> Self { self._beta = Some(beta.to_f64().unwrap_or(0.0)); self } pub fn with_duration(mut self, duration: Decimal) -> Self { self._duration = Some(duration.to_f64().unwrap_or(0.0)); self } // Position type convenience methods pub fn long_position(mut self, quantity: i64) -> Self { self.quantity = quantity.abs() as f64; self.market_value = self.quantity * self.market_price; self.unrealized_pnl = (self.market_price - self.average_cost) * self.quantity; self } pub fn short_position(mut self, quantity: i64) -> Self { self.quantity = -(quantity.abs() as f64); self.market_value = self.quantity * self.market_price; self.unrealized_pnl = (self.market_price - self.average_cost) * self.quantity; self } pub fn profitable(mut self, profit_pct: f64) -> Self { let profit_multiplier = 1.0 + (profit_pct / 100.0); let new_market_price = self.average_cost * profit_multiplier; self.market_price = new_market_price; self.market_value = self.quantity * new_market_price; self.unrealized_pnl = (new_market_price - self.average_cost) * self.quantity; self } pub fn losing(mut self, loss_pct: f64) -> Self { let loss_multiplier = 1.0 - (loss_pct / 100.0); let new_market_price = self.average_cost * loss_multiplier; self.market_price = new_market_price; self.market_value = self.quantity * new_market_price; self.unrealized_pnl = (new_market_price - self.average_cost) * self.quantity; self } pub fn build(self) -> Position { Position { symbol: self.symbol, quantity: self.quantity, market_price: self.market_price, market_value: self.market_value, average_cost: self.average_cost, average_price: self.average_price, unrealized_pnl: self.unrealized_pnl, realized_pnl: self.realized_pnl, last_updated: self.last_updated, } } } // ============================================================================= // COUNTERPARTY BUILDER // ============================================================================= /// Builder for creating test Counterparty objects #[derive(Debug, Clone)] pub struct CounterpartyBuilder { id: String, name: String, counterparty_type: String, country: String, credit_rating: Option, lei_code: Option, parent_company: Option, is_active: bool, exposure_limit: Option, margin_requirement: Option, netting_agreement: bool, created_at: DateTime, updated_at: DateTime, metadata: serde_json::Value, } impl Default for CounterpartyBuilder { fn default() -> Self { Self::new() } } impl CounterpartyBuilder { pub fn new() -> Self { let now = Utc::now(); Self { id: "TEST_COUNTERPARTY_001".to_string(), name: "Test Counterparty".to_string(), counterparty_type: "Bank".to_string(), country: "US".to_string(), credit_rating: Some("AA".to_string()), lei_code: None, parent_company: None, is_active: true, exposure_limit: Some(Decimal::from(10000000)), // $10M margin_requirement: Some(Decimal::new(5, 2)), // 5% netting_agreement: true, created_at: now, updated_at: now, metadata: json!({"test_data": true}), } } pub fn with_id(mut self, id: impl Into) -> Self { self.id = id.into(); self } pub fn with_name(mut self, name: impl Into) -> Self { self.name = name.into(); self } pub fn with_type(mut self, counterparty_type: impl Into) -> Self { self.counterparty_type = counterparty_type.into(); self } pub fn with_country(mut self, country: impl Into) -> Self { self.country = country.into(); self } pub fn with_credit_rating(mut self, rating: impl Into) -> Self { self.credit_rating = Some(rating.into()); self } pub fn with_exposure_limit(mut self, limit: Decimal) -> Self { self.exposure_limit = Some(limit); self } pub fn with_margin_requirement(mut self, margin: Decimal) -> Self { self.margin_requirement = Some(margin); self } pub fn without_netting_agreement(mut self) -> Self { self.netting_agreement = false; self } pub fn inactive(mut self) -> Self { self.is_active = false; self } // Counterparty type convenience methods pub fn bank(mut self) -> Self { self.counterparty_type = "Bank".to_string(); self.credit_rating = Some("AA".to_string()); self } pub fn broker(mut self) -> Self { self.counterparty_type = "Broker".to_string(); self.credit_rating = Some("A".to_string()); self } pub fn exchange(mut self) -> Self { self.counterparty_type = "Exchange".to_string(); self.credit_rating = Some("AAA".to_string()); self } pub fn hedge_fund(mut self) -> Self { self.counterparty_type = "Hedge Fund".to_string(); self.credit_rating = Some("BBB".to_string()); self } pub fn build(self) -> Counterparty { Counterparty { id: self.id, name: self.name, counterparty_type: self.counterparty_type, country: self.country, credit_rating: self.credit_rating, lei_code: self.lei_code, parent_company: self.parent_company, is_active: self.is_active, exposure_limit: self.exposure_limit, margin_requirement: self.margin_requirement, netting_agreement: self.netting_agreement, created_at: self.created_at, updated_at: self.updated_at, metadata: self.metadata, } } } // ============================================================================= // BATCH BUILDERS // ============================================================================= /// Utility for building multiple test objects #[derive(Debug)] pub struct BatchBuilder; impl BatchBuilder { /// Create multiple test instruments with different asset classes pub fn create_diverse_instruments(count: usize) -> Vec { let asset_classes = [ AssetClass::Equities, AssetClass::Currencies, AssetClass::FixedIncome, AssetClass::Derivatives, AssetClass::Commodities, AssetClass::Alternatives, ]; (0..count) .map(|i| { let asset_class = asset_classes[i % asset_classes.len()]; let symbol = generate_test_symbol(asset_class); let mut builder = InstrumentBuilder::new() .with_symbol(&symbol) .with_name(format!("Test Instrument {}", i + 1)); builder = match asset_class { AssetClass::Equities => builder.equity(), AssetClass::Currencies => builder.currency(), AssetClass::FixedIncome => builder.bond(), AssetClass::Derivatives => builder.future(), AssetClass::Commodities => builder.commodity(), AssetClass::Alternatives => builder.crypto(), AssetClass::Cash => builder.equity(), // Default to equity for cash }; builder.build() }) .collect() } /// Create multiple test portfolios pub fn create_test_portfolios(count: usize) -> Vec { (0..count) .map(|i| { PortfolioBuilder::new() .with_id(format!("TEST_PORTFOLIO_{:03}", i + 1)) .with_name(format!("Test Portfolio {}", i + 1)) .with_manager_id(format!("test_manager_{}", i + 1)) .build() }) .collect() } /// Create multiple test positions for a portfolio pub fn create_test_positions(portfolio_id: &str, symbols: &[&str]) -> Vec { symbols .iter() .enumerate() .map(|(i, &symbol)| { let quantity = Decimal::from((i + 1) * 100); let price = get_test_price_for_symbol(symbol).to_decimal(); PositionBuilder::new() .with_portfolio_id(portfolio_id) .with_symbol(symbol) .with_quantity(quantity) .with_average_price(price) .with_market_price(price) .build() }) .collect() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_instrument_builder() { let instrument = InstrumentBuilder::new() .with_symbol("TEST_SYMBOL") .with_name("Test Name") .equity() .build(); assert_eq!(instrument.symbol, "TEST_SYMBOL"); assert_eq!(instrument.name, "Test Name"); assert_eq!(instrument.instrument_type, InstrumentType::Equity); assert_eq!(instrument.asset_class, AssetClass::Equities); assert!(instrument.is_active); } #[test] fn test_portfolio_builder() { let portfolio = PortfolioBuilder::new() .with_id("TEST_PORT") .with_name("Test Portfolio") .strategy_portfolio() .build(); assert_eq!(portfolio.id, "TEST_PORT"); assert_eq!(portfolio.name, "Test Portfolio"); assert_eq!(portfolio.portfolio_type, "Strategy"); assert!(portfolio.is_active); } #[test] fn test_position_builder() { let position = PositionBuilder::new() .with_symbol("TEST") .long_position(500) .profitable(10.0) .build(); assert_eq!(position.symbol, "TEST"); assert_eq!(position.quantity, 500.0); assert!(position.unrealized_pnl > 0.0); } #[test] fn test_batch_builder() { let instruments = BatchBuilder::create_diverse_instruments(6); assert_eq!(instruments.len(), 6); // Should have different asset classes let asset_classes: std::collections::HashSet<_> = instruments.iter().map(|i| i.asset_class).collect(); assert!(asset_classes.len() > 1); } }