Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade Files updated: - Cargo.lock: Dependency resolution for Tonic 0.14.2 - All build.rs: Updated for tonic-prost-build - Proto files: Regenerated with tonic-prost 0.14 - Examples/tests: Updated for new gRPC API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
617 lines
19 KiB
Rust
617 lines
19 KiB
Rust
//! Critical Risk Management Validation Tests
|
|
//!
|
|
//! These tests validate the core risk management components for production readiness,
|
|
//! focusing on the critical components identified in the analysis:
|
|
//! - VaR calculations (Historical, Parametric, Monte Carlo, Hybrid)
|
|
//! - Kelly criterion sizing
|
|
//! - Position limits and concentration risk
|
|
//! - Atomic kill switch functionality
|
|
//! - Stress testing scenarios
|
|
//! - Regulatory compliance (MiFID II, Basel III, Dodd-Frank)
|
|
#![allow(unused_crate_dependencies)]
|
|
|
|
use std::collections::HashMap;
|
|
use std::time::Duration;
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal::prelude::FromStr;
|
|
use tokio::time::timeout;
|
|
|
|
// Import common types
|
|
use common::{OrderSide, OrderType, Symbol};
|
|
|
|
// Import risk-specific types
|
|
use risk::risk_types::{KillSwitchScope, OrderInfo};
|
|
// Fixed: Use re-exported types from risk crate root
|
|
use risk::{ComplianceEngine, VaRCalculator, KellySizer, RiskEngine, AtomicKillSwitch, StressTester};
|
|
// Use RealVaREngine alias for backward compatibility
|
|
use risk::RealVaREngine;
|
|
|
|
// Test-specific data structures
|
|
#[derive(Debug, Clone)]
|
|
struct PositionInfo {
|
|
quantity: Decimal,
|
|
avg_price: Decimal,
|
|
market_value: Decimal,
|
|
unrealized_pnl: Decimal,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct HistoricalPrice {
|
|
symbol: Symbol,
|
|
price: Decimal,
|
|
timestamp: chrono::DateTime<chrono::Utc>,
|
|
volume: Decimal,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Portfolio {
|
|
id: String,
|
|
total_value: Decimal,
|
|
positions: HashMap<Symbol, PositionInfo>,
|
|
cash_balance: Decimal,
|
|
unrealized_pnl: Decimal,
|
|
daily_pnl: Decimal,
|
|
}
|
|
|
|
/// Test data constants for reproducible testing
|
|
const TEST_SYMBOL: &str = "EURUSD";
|
|
const TEST_ACCOUNT_ID: &str = "test_account_001";
|
|
const TEST_PORTFOLIO_ID: &str = "test_portfolio_001";
|
|
const STRESS_TEST_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
#[tokio::test]
|
|
async fn test_var_calculation_comprehensive() {
|
|
let var_engine = create_test_var_engine().await;
|
|
let test_positions = create_test_positions();
|
|
let historical_prices = create_test_historical_data();
|
|
|
|
// Test all VaR methodologies
|
|
let result = var_engine
|
|
.calculate_comprehensive_var(TEST_PORTFOLIO_ID, &test_positions, &historical_prices)
|
|
.await
|
|
.expect("VaR calculation should succeed");
|
|
|
|
// Validate all VaR methods produce reasonable results
|
|
assert!(
|
|
result.historical_var > Decimal::ZERO,
|
|
"Historical VaR must be positive"
|
|
);
|
|
assert!(
|
|
result.parametric_var > Decimal::ZERO,
|
|
"Parametric VaR must be positive"
|
|
);
|
|
assert!(
|
|
result.monte_carlo_var > Decimal::ZERO,
|
|
"Monte Carlo VaR must be positive"
|
|
);
|
|
assert!(
|
|
result.hybrid_var > Decimal::ZERO,
|
|
"Hybrid VaR must be positive"
|
|
);
|
|
|
|
// VaR should be reasonable (between 0.1% and 10% of portfolio value)
|
|
let portfolio_value = calculate_portfolio_value(&test_positions);
|
|
let var_ratio = result.hybrid_var / portfolio_value;
|
|
assert!(
|
|
var_ratio >= Decimal::from_str("0.001").unwrap(),
|
|
"VaR too low - may be miscalculated"
|
|
);
|
|
assert!(
|
|
var_ratio <= Decimal::from_str("0.10").unwrap(),
|
|
"VaR too high - may indicate error"
|
|
);
|
|
|
|
// Hybrid VaR should be within reasonable bounds of other methods
|
|
let max_var = [
|
|
result.historical_var,
|
|
result.parametric_var,
|
|
result.monte_carlo_var,
|
|
]
|
|
.iter()
|
|
.max()
|
|
.unwrap();
|
|
let min_var = [
|
|
result.historical_var,
|
|
result.parametric_var,
|
|
result.monte_carlo_var,
|
|
]
|
|
.iter()
|
|
.min()
|
|
.unwrap();
|
|
|
|
assert!(
|
|
result.hybrid_var >= *min_var,
|
|
"Hybrid VaR below minimum component"
|
|
);
|
|
assert!(
|
|
result.hybrid_var <= *max_var,
|
|
"Hybrid VaR above maximum component"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_kelly_criterion_sizing() {
|
|
let kelly_sizer = create_test_kelly_sizer().await;
|
|
let symbol = Symbol::from(TEST_SYMBOL);
|
|
|
|
// Test with profitable strategy parameters
|
|
let result = kelly_sizer
|
|
.calculate_kelly_fraction(&symbol, "profitable_strategy")
|
|
.await
|
|
.expect("Kelly calculation should succeed");
|
|
|
|
// Kelly fraction should be reasonable for profitable strategy
|
|
assert!(
|
|
result.kelly_fraction > Decimal::ZERO,
|
|
"Kelly fraction should be positive for profitable strategy"
|
|
);
|
|
assert!(
|
|
result.kelly_fraction <= Decimal::ONE,
|
|
"Kelly fraction should not exceed 100%"
|
|
);
|
|
|
|
// Fractional Kelly should be applied (typically 25% of full Kelly)
|
|
assert!(
|
|
result.recommended_fraction < result.kelly_fraction,
|
|
"Recommended should be less than full Kelly"
|
|
);
|
|
assert!(
|
|
result.recommended_fraction >= result.kelly_fraction * Decimal::from_str("0.1").unwrap(),
|
|
"Recommended fraction too conservative"
|
|
);
|
|
|
|
// Test with losing strategy parameters
|
|
let losing_result = kelly_sizer
|
|
.calculate_kelly_fraction(&symbol, "losing_strategy")
|
|
.await
|
|
.expect("Kelly calculation should succeed for losing strategy");
|
|
|
|
assert!(
|
|
losing_result.kelly_fraction <= Decimal::ZERO,
|
|
"Kelly fraction should be zero or negative for losing strategy"
|
|
);
|
|
assert!(
|
|
losing_result.recommended_fraction == Decimal::ZERO,
|
|
"No position recommended for losing strategy"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_position_limits_enforcement() {
|
|
let risk_engine = create_test_risk_engine().await;
|
|
|
|
// Test normal position within limits
|
|
let normal_order = create_test_order(Decimal::from_str("10000").unwrap()); // $10k position
|
|
let result = risk_engine
|
|
.check_pre_trade_risk(&normal_order, TEST_ACCOUNT_ID)
|
|
.await
|
|
.expect("Risk check should succeed");
|
|
|
|
assert!(result.approved, "Normal position should be approved");
|
|
assert!(
|
|
result.risk_warnings.is_empty(),
|
|
"No warnings for normal position"
|
|
);
|
|
|
|
// Test position exceeding single instrument limit
|
|
let large_order = create_test_order(Decimal::from_str("1000000").unwrap()); // $1M position
|
|
let large_result = risk_engine
|
|
.check_pre_trade_risk(&large_order, TEST_ACCOUNT_ID)
|
|
.await
|
|
.expect("Risk check should succeed");
|
|
|
|
assert!(!large_result.approved, "Large position should be rejected");
|
|
assert!(
|
|
large_result
|
|
.risk_warnings
|
|
.iter()
|
|
.any(|w| w.contains("position limit")),
|
|
"Should warn about position limits"
|
|
);
|
|
|
|
// Test concentration risk (too much in single instrument)
|
|
let concentration_order = create_concentration_test_order();
|
|
let conc_result = risk_engine
|
|
.check_pre_trade_risk(&concentration_order, TEST_ACCOUNT_ID)
|
|
.await
|
|
.expect("Risk check should succeed");
|
|
|
|
assert!(
|
|
!conc_result.approved,
|
|
"Concentrated position should be rejected"
|
|
);
|
|
assert!(
|
|
conc_result
|
|
.risk_warnings
|
|
.iter()
|
|
.any(|w| w.contains("concentration")),
|
|
"Should warn about concentration risk"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_atomic_kill_switch_functionality() {
|
|
let kill_switch = create_test_kill_switch().await;
|
|
|
|
// Initially trading should be allowed
|
|
assert!(
|
|
kill_switch.is_trading_allowed(&KillSwitchScope::Global),
|
|
"Trading should initially be allowed"
|
|
);
|
|
assert!(
|
|
kill_switch.is_trading_allowed(&KillSwitchScope::Account(TEST_ACCOUNT_ID.to_string())),
|
|
"Account trading should initially be allowed"
|
|
);
|
|
|
|
// Test global halt
|
|
kill_switch
|
|
.emergency_halt(KillSwitchScope::Global, "Test global halt")
|
|
.await
|
|
.expect("Global halt should succeed");
|
|
|
|
assert!(
|
|
!kill_switch.is_trading_allowed(&KillSwitchScope::Global),
|
|
"Global halt should prevent all trading"
|
|
);
|
|
assert!(
|
|
!kill_switch.is_trading_allowed(&KillSwitchScope::Account(TEST_ACCOUNT_ID.to_string())),
|
|
"Global halt should prevent account trading"
|
|
);
|
|
|
|
// Test kill switch performance (must be sub-microsecond)
|
|
use std::time::Instant;
|
|
let start = Instant::now();
|
|
for _ in 0..1000 {
|
|
kill_switch.is_trading_allowed(&KillSwitchScope::Global);
|
|
}
|
|
let elapsed = start.elapsed();
|
|
let avg_nanos = elapsed.as_nanos() / 1000;
|
|
|
|
assert!(
|
|
avg_nanos < 1000,
|
|
"Kill switch check must be sub-microsecond, got {}ns",
|
|
avg_nanos
|
|
);
|
|
|
|
// Test recovery
|
|
kill_switch
|
|
.resume_trading(KillSwitchScope::Global, "Test recovery")
|
|
.await
|
|
.expect("Trading resume should succeed");
|
|
|
|
assert!(
|
|
kill_switch.is_trading_allowed(&KillSwitchScope::Global),
|
|
"Trading should resume after recovery"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_stress_testing_scenarios() {
|
|
let stress_tester = create_test_stress_tester().await;
|
|
let test_portfolio = create_test_portfolio();
|
|
|
|
// Test 2008 Financial Crisis scenario
|
|
let crisis_result = timeout(
|
|
STRESS_TEST_TIMEOUT,
|
|
stress_tester.run_stress_test("2008_crisis", &test_portfolio),
|
|
)
|
|
.await
|
|
.expect("Stress test should not timeout")
|
|
.expect("2008 crisis stress test should succeed");
|
|
|
|
assert!(
|
|
crisis_result.portfolio_loss > Decimal::ZERO,
|
|
"Crisis scenario should show losses"
|
|
);
|
|
assert!(
|
|
crisis_result.max_drawdown > Decimal::ZERO,
|
|
"Should calculate max drawdown"
|
|
);
|
|
assert!(
|
|
crisis_result.var_breach_probability > Decimal::ZERO,
|
|
"Should show VaR breach probability"
|
|
);
|
|
|
|
// Stress test loss should be significant but not total portfolio destruction
|
|
let loss_ratio = crisis_result.portfolio_loss / test_portfolio.total_value;
|
|
assert!(
|
|
loss_ratio > Decimal::from_str("0.05").unwrap(),
|
|
"Crisis should cause >5% loss"
|
|
);
|
|
assert!(
|
|
loss_ratio < Decimal::from_str("0.90").unwrap(),
|
|
"Crisis should not destroy >90% of portfolio"
|
|
);
|
|
|
|
// Test COVID-19 Flash Crash scenario
|
|
let covid_result = timeout(
|
|
STRESS_TEST_TIMEOUT,
|
|
stress_tester.run_stress_test("covid_crash", &test_portfolio),
|
|
)
|
|
.await
|
|
.expect("COVID stress test should not timeout")
|
|
.expect("COVID stress test should succeed");
|
|
|
|
assert!(
|
|
covid_result.portfolio_loss > Decimal::ZERO,
|
|
"COVID scenario should show losses"
|
|
);
|
|
|
|
// Test Flash Crash scenario (high-frequency event)
|
|
let flash_result = timeout(
|
|
STRESS_TEST_TIMEOUT,
|
|
stress_tester.run_stress_test("flash_crash", &test_portfolio),
|
|
)
|
|
.await
|
|
.expect("Flash crash test should not timeout")
|
|
.expect("Flash crash test should succeed");
|
|
|
|
assert!(
|
|
flash_result.portfolio_loss > Decimal::ZERO,
|
|
"Flash crash should show losses"
|
|
);
|
|
assert!(
|
|
flash_result.time_to_recovery.is_some(),
|
|
"Should estimate recovery time"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regulatory_compliance() {
|
|
let compliance_engine = create_test_compliance_engine().await;
|
|
|
|
// Test MiFID II compliance
|
|
let mifid_result = compliance_engine
|
|
.validate_mifid_ii_compliance(TEST_ACCOUNT_ID)
|
|
.await
|
|
.expect("MiFID II validation should succeed");
|
|
|
|
assert!(mifid_result.is_compliant, "Should be MiFID II compliant");
|
|
assert!(
|
|
mifid_result.best_execution_documented,
|
|
"Best execution must be documented"
|
|
);
|
|
assert!(
|
|
mifid_result.client_categorization_valid,
|
|
"Client categorization must be valid"
|
|
);
|
|
|
|
// Test Basel III compliance
|
|
let basel_result = compliance_engine
|
|
.validate_basel_iii_compliance(TEST_PORTFOLIO_ID)
|
|
.await
|
|
.expect("Basel III validation should succeed");
|
|
|
|
assert!(basel_result.is_compliant, "Should be Basel III compliant");
|
|
assert!(
|
|
basel_result.capital_adequacy_ratio > Decimal::from_str("0.08").unwrap(),
|
|
"Capital adequacy ratio must exceed 8%"
|
|
);
|
|
assert!(
|
|
basel_result.leverage_ratio > Decimal::from_str("0.03").unwrap(),
|
|
"Leverage ratio must exceed 3%"
|
|
);
|
|
|
|
// Test Dodd-Frank compliance
|
|
let dodd_frank_result = compliance_engine
|
|
.validate_dodd_frank_compliance(TEST_ACCOUNT_ID)
|
|
.await
|
|
.expect("Dodd-Frank validation should succeed");
|
|
|
|
assert!(
|
|
dodd_frank_result.is_compliant,
|
|
"Should be Dodd-Frank compliant"
|
|
);
|
|
assert!(
|
|
dodd_frank_result.volcker_rule_compliant,
|
|
"Must comply with Volcker rule"
|
|
);
|
|
assert!(
|
|
dodd_frank_result.swap_reporting_compliant,
|
|
"Swap reporting must be compliant"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_circuit_breaker_conditions() {
|
|
let risk_engine = create_test_risk_engine().await;
|
|
|
|
// Simulate 2% daily loss to trigger circuit breaker
|
|
let loss_order = create_loss_triggering_order();
|
|
let result = risk_engine
|
|
.check_pre_trade_risk(&loss_order, TEST_ACCOUNT_ID)
|
|
.await
|
|
.expect("Risk check should succeed");
|
|
|
|
assert!(
|
|
!result.approved,
|
|
"Order triggering 2% loss should be rejected"
|
|
);
|
|
assert!(
|
|
result
|
|
.risk_warnings
|
|
.iter()
|
|
.any(|w| w.contains("circuit breaker")),
|
|
"Should trigger circuit breaker warning"
|
|
);
|
|
|
|
// Verify kill switch is activated for account
|
|
let kill_switch = risk_engine.get_kill_switch();
|
|
assert!(
|
|
!kill_switch.is_trading_allowed(&KillSwitchScope::Account(TEST_ACCOUNT_ID.to_string())),
|
|
"Circuit breaker should halt account trading"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_performance_requirements() {
|
|
let risk_engine = create_test_risk_engine().await;
|
|
let test_order = create_test_order(Decimal::from_str("10000").unwrap());
|
|
|
|
// Test that risk checks meet HFT latency requirements
|
|
use std::time::Instant;
|
|
let start = Instant::now();
|
|
|
|
// Run 1000 risk checks to get average latency
|
|
for _ in 0..1000 {
|
|
let _result = risk_engine
|
|
.check_pre_trade_risk(&test_order, TEST_ACCOUNT_ID)
|
|
.await
|
|
.expect("Risk check should succeed");
|
|
}
|
|
|
|
let elapsed = start.elapsed();
|
|
let avg_micros = elapsed.as_micros() / 1000;
|
|
|
|
// Risk checks should be sub-50μs for HFT requirements
|
|
// Note: This validates the performance concern identified in the analysis
|
|
if avg_micros > 50 {
|
|
eprintln!(
|
|
"WARNING: Risk check latency {}μs exceeds 50μs HFT target",
|
|
avg_micros
|
|
);
|
|
eprintln!("This confirms the performance concern identified in the analysis");
|
|
eprintln!("VaR calculations are the likely bottleneck - consider caching or approximation");
|
|
}
|
|
|
|
// At minimum, should be under 1ms for any production use
|
|
assert!(
|
|
avg_micros < 1000,
|
|
"Risk check latency {}μs exceeds 1ms maximum",
|
|
avg_micros
|
|
);
|
|
}
|
|
|
|
// Helper functions for test setup
|
|
async fn create_test_var_engine() -> RealVaREngine {
|
|
RealVaREngine::new()
|
|
.await
|
|
.expect("VaR engine creation should succeed")
|
|
}
|
|
|
|
async fn create_test_kelly_sizer() -> KellySizer {
|
|
KellySizer::new()
|
|
.await
|
|
.expect("Kelly sizer creation should succeed")
|
|
}
|
|
|
|
async fn create_test_risk_engine() -> RiskEngine {
|
|
RiskEngine::new()
|
|
.await
|
|
.expect("Risk engine creation should succeed")
|
|
}
|
|
|
|
async fn create_test_kill_switch() -> AtomicKillSwitch {
|
|
AtomicKillSwitch::new()
|
|
.await
|
|
.expect("Kill switch creation should succeed")
|
|
}
|
|
|
|
async fn create_test_stress_tester() -> StressTester {
|
|
StressTester::new()
|
|
.await
|
|
.expect("Stress tester creation should succeed")
|
|
}
|
|
|
|
async fn create_test_compliance_engine() -> ComplianceEngine {
|
|
ComplianceEngine::new()
|
|
.await
|
|
.expect("Compliance engine creation should succeed")
|
|
}
|
|
|
|
fn create_test_positions() -> HashMap<Symbol, PositionInfo> {
|
|
let mut positions = HashMap::new();
|
|
positions.insert(
|
|
Symbol::from(TEST_SYMBOL),
|
|
PositionInfo {
|
|
quantity: Decimal::from_str("100000").unwrap(),
|
|
avg_price: Decimal::from_str("1.1050").unwrap(),
|
|
market_value: Decimal::from_str("110500").unwrap(),
|
|
unrealized_pnl: Decimal::from_str("500").unwrap(),
|
|
},
|
|
);
|
|
positions
|
|
}
|
|
|
|
fn create_test_historical_data() -> HashMap<Symbol, Vec<HistoricalPrice>> {
|
|
let mut data = HashMap::new();
|
|
let symbol = Symbol::from(TEST_SYMBOL);
|
|
|
|
// Create 30 days of synthetic price data with some volatility
|
|
let mut prices = Vec::new();
|
|
let base_price = 1.1050;
|
|
for i in 0..30 {
|
|
prices.push(HistoricalPrice {
|
|
symbol: symbol.clone(),
|
|
price: Decimal::try_from(
|
|
base_price + (i as f64 * 0.001) + (i as f64 % 5) * 0.0005 - 0.001,
|
|
)
|
|
.unwrap(),
|
|
timestamp: chrono::Utc::now() - chrono::Duration::days(30 - i),
|
|
volume: Decimal::from_str("1000000").unwrap(),
|
|
});
|
|
}
|
|
|
|
data.insert(symbol, prices);
|
|
data
|
|
}
|
|
|
|
fn create_test_order(size: Decimal) -> OrderInfo {
|
|
OrderInfo {
|
|
order_id: format!("test_order_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)),
|
|
symbol: Symbol::from(TEST_SYMBOL),
|
|
instrument_id: format!("inst_{}", TEST_SYMBOL),
|
|
side: OrderSide::Buy,
|
|
quantity: size / Decimal::from_str("1.1050").unwrap(), // Convert to units
|
|
price: Decimal::from_str("1.1050").unwrap(),
|
|
order_type: Some(OrderType::Market),
|
|
portfolio_id: Some(TEST_PORTFOLIO_ID.to_string()),
|
|
strategy_id: None,
|
|
}
|
|
}
|
|
|
|
fn create_concentration_test_order() -> OrderInfo {
|
|
// Create order that would exceed concentration limits (>20% of portfolio)
|
|
OrderInfo {
|
|
order_id: format!("conc_test_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)),
|
|
symbol: Symbol::from(TEST_SYMBOL),
|
|
instrument_id: format!("inst_{}", TEST_SYMBOL),
|
|
side: OrderSide::Buy,
|
|
quantity: Decimal::from_str("500000").unwrap(), // Large position
|
|
price: Decimal::from_str("1.1050").unwrap(),
|
|
order_type: Some(OrderType::Market),
|
|
portfolio_id: Some(TEST_PORTFOLIO_ID.to_string()),
|
|
strategy_id: None,
|
|
}
|
|
}
|
|
|
|
fn create_loss_triggering_order() -> OrderInfo {
|
|
// Create order that would trigger 2% daily loss circuit breaker
|
|
OrderInfo {
|
|
order_id: format!("loss_test_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)),
|
|
symbol: Symbol::from(TEST_SYMBOL),
|
|
instrument_id: format!("inst_{}", TEST_SYMBOL),
|
|
side: OrderSide::Sell,
|
|
quantity: Decimal::from_str("200000").unwrap(), // Position that would cause 2%+ loss
|
|
price: Decimal::from_str("1.0800").unwrap(), // Below current market
|
|
order_type: Some(OrderType::Market),
|
|
portfolio_id: Some(TEST_PORTFOLIO_ID.to_string()),
|
|
strategy_id: None,
|
|
}
|
|
}
|
|
|
|
fn create_test_portfolio() -> Portfolio {
|
|
Portfolio {
|
|
id: TEST_PORTFOLIO_ID.to_string(),
|
|
total_value: Decimal::from_str("1000000").unwrap(), // $1M portfolio
|
|
positions: create_test_positions(),
|
|
cash_balance: Decimal::from_str("100000").unwrap(),
|
|
unrealized_pnl: Decimal::from_str("2500").unwrap(),
|
|
daily_pnl: Decimal::from_str("1200").unwrap(),
|
|
}
|
|
}
|
|
|
|
fn calculate_portfolio_value(positions: &HashMap<Symbol, PositionInfo>) -> Decimal {
|
|
positions.values().map(|p| p.market_value).sum()
|
|
}
|