Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
858 lines
26 KiB
Rust
858 lines
26 KiB
Rust
//! 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<String>,
|
|
cusip: Option<String>,
|
|
bloomberg_id: Option<String>,
|
|
reuters_id: Option<String>,
|
|
name: String,
|
|
instrument_type: InstrumentType,
|
|
asset_class: AssetClass,
|
|
sector: Option<MarketSector>,
|
|
currency: String,
|
|
exchange: Option<String>,
|
|
tick_size: Option<Decimal>,
|
|
lot_size: Option<Decimal>,
|
|
multiplier: Option<Decimal>,
|
|
maturity_date: Option<DateTime<Utc>>,
|
|
strike_price: Option<Decimal>,
|
|
option_type: Option<String>,
|
|
underlying_symbol: Option<String>,
|
|
is_active: bool,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
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<String>) -> Self {
|
|
self.symbol = symbol.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_name(mut self, name: impl Into<String>) -> Self {
|
|
self.name = name.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_currency(mut self, currency: impl Into<String>) -> Self {
|
|
self.currency = currency.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_exchange(mut self, exchange: impl Into<String>) -> 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<Utc>) -> 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<String>) -> Self {
|
|
self.option_type = Some(option_type.into());
|
|
self
|
|
}
|
|
|
|
pub fn with_underlying_symbol(mut self, underlying_symbol: impl Into<String>) -> 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<String>,
|
|
base_currency: String,
|
|
portfolio_type: String,
|
|
inception_date: DateTime<Utc>,
|
|
manager_id: String,
|
|
benchmark: Option<String>,
|
|
risk_budget: Option<Decimal>,
|
|
var_limit: Option<Decimal>,
|
|
max_drawdown_limit: Option<Decimal>,
|
|
is_active: bool,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
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<String>) -> Self {
|
|
self.id = id.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_name(mut self, name: impl Into<String>) -> Self {
|
|
self.name = name.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
|
self.description = Some(description.into());
|
|
self
|
|
}
|
|
|
|
pub fn with_base_currency(mut self, currency: impl Into<String>) -> Self {
|
|
self.base_currency = currency.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_portfolio_type(mut self, portfolio_type: impl Into<String>) -> Self {
|
|
self.portfolio_type = portfolio_type.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_inception_date(mut self, date: DateTime<Utc>) -> Self {
|
|
self.inception_date = date;
|
|
self
|
|
}
|
|
|
|
pub fn with_manager_id(mut self, manager_id: impl Into<String>) -> Self {
|
|
self.manager_id = manager_id.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_benchmark(mut self, benchmark: impl Into<String>) -> 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<String>,
|
|
_weight: Option<f64>,
|
|
_beta: Option<f64>,
|
|
_duration: Option<f64>,
|
|
}
|
|
|
|
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<String>) -> Self {
|
|
self._portfolio_id = Some(portfolio_id.into());
|
|
self
|
|
}
|
|
|
|
pub fn with_symbol(mut self, symbol: impl Into<String>) -> 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<String>,
|
|
lei_code: Option<String>,
|
|
parent_company: Option<String>,
|
|
is_active: bool,
|
|
exposure_limit: Option<Decimal>,
|
|
margin_requirement: Option<Decimal>,
|
|
netting_agreement: bool,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
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<String>) -> Self {
|
|
self.id = id.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_name(mut self, name: impl Into<String>) -> Self {
|
|
self.name = name.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_type(mut self, counterparty_type: impl Into<String>) -> Self {
|
|
self.counterparty_type = counterparty_type.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_country(mut self, country: impl Into<String>) -> Self {
|
|
self.country = country.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_credit_rating(mut self, rating: impl Into<String>) -> 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<Instrument> {
|
|
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<Portfolio> {
|
|
(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<Position> {
|
|
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);
|
|
}
|
|
}
|