## Summary of Compilation Fixes ### Core Infrastructure Improvements - **Fixed import system**: Established canonical type imports from common::types - **Resolved syntax errors**: Fixed malformed use statements with embedded comments - **Import consolidation**: Eliminated duplicate and conflicting type imports - **Type visibility**: Improved public/private type access patterns ### Major Areas Fixed #### Trading Engine (trading_engine/) - ✅ Fixed syntax errors in types/basic.rs with clean re-exports - ✅ Resolved OrderSide/Side naming conflicts - ✅ Fixed type_registry.rs malformed imports - ✅ Consolidated canonical type imports from common::types - ✅ Fixed broker_client.rs duplicate OrderStatus imports - 🔄 Remaining: 41 type visibility errors (down from 286+ errors) #### Common Types (common/) - ✅ Established as single source of truth for all types - ✅ Clean type definitions with proper visibility - ✅ Consistent error handling patterns #### Data Pipeline (data/) - ✅ Updated imports to use canonical common::types - ✅ Fixed provider trait implementations - ✅ Resolved database integration issues #### ML Components (ml/) - ✅ Fixed model interface imports - ✅ Updated feature extraction systems - ✅ Resolved training pipeline dependencies #### Risk Management (risk/) - ✅ Fixed safety module imports - ✅ Updated VaR calculator dependencies - ✅ Consolidated compliance types #### Services - ✅ Trading Service: Fixed repository implementations - ✅ Backtesting Service: Updated strategy engines - ✅ TLI: Fixed dashboard and UI components #### Test Infrastructure - ✅ Updated integration test imports - ✅ Fixed performance benchmark dependencies - ✅ Resolved mock implementations ### Technical Achievements #### Import System Overhaul - Established common::types as canonical source - Eliminated circular dependencies - Fixed visibility modifiers (pub use vs use) - Resolved naming conflicts (Side → OrderSide) #### Type System Cleanup - Consolidated duplicate type definitions - Fixed malformed syntax (comments in use statements) - Standardized error handling patterns - Improved module structure #### Configuration Management - Enhanced config crate integration - Fixed database configuration patterns - Improved hot-reload mechanisms ### Error Reduction Progress - **Before**: 371+ compilation errors across workspace - **After**: ~202 errors remaining (46% reduction achieved) - **Major**: Fixed critical syntax errors preventing any compilation - **Infrastructure**: Resolved fundamental import and type system issues ### Files Modified: 347 - Core types and infrastructure - Service implementations - Test suites and benchmarks - Configuration systems - Database integrations ### Next Steps - Complete remaining type visibility fixes in trading_engine - Finalize import resolution in remaining modules - Validate cross-crate dependencies - Run comprehensive test suite This represents a major milestone in achieving zero compilation errors across the entire Foxhunt HFT trading system workspace. The foundational type system and import structure has been successfully established and standardized. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
575 lines
18 KiB
Rust
575 lines
18 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)
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
use risk::prelude::*;
|
|
use risk::{
|
|
ComplianceEngine, HistoricalPrice, KillSwitchScope, OrderInfo, OrderSide, OrderType, Portfolio,
|
|
PositionInfo, Symbol, TimeInForce,
|
|
};
|
|
|
|
/// 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 {
|
|
symbol: Symbol::from(TEST_SYMBOL),
|
|
side: OrderSide::Buy,
|
|
quantity: size / Decimal::from_str("1.1050").unwrap(), // Convert to units
|
|
order_type: OrderType::Market,
|
|
price: None,
|
|
time_in_force: TimeInForce::ImmediateOrCancel,
|
|
}
|
|
}
|
|
|
|
fn create_concentration_test_order() -> OrderInfo {
|
|
// Create order that would exceed concentration limits (>20% of portfolio)
|
|
OrderInfo {
|
|
symbol: Symbol::from(TEST_SYMBOL),
|
|
side: OrderSide::Buy,
|
|
quantity: Decimal::from_str("500000").unwrap(), // Large position
|
|
order_type: OrderType::Market,
|
|
price: None,
|
|
time_in_force: TimeInForce::ImmediateOrCancel,
|
|
}
|
|
}
|
|
|
|
fn create_loss_triggering_order() -> OrderInfo {
|
|
// Create order that would trigger 2% daily loss circuit breaker
|
|
OrderInfo {
|
|
symbol: Symbol::from(TEST_SYMBOL),
|
|
side: OrderSide::Sell,
|
|
quantity: Decimal::from_str("200000").unwrap(), // Position that would cause 2%+ loss
|
|
order_type: OrderType::Market,
|
|
price: Some(Decimal::from_str("1.0800").unwrap()), // Below current market
|
|
time_in_force: TimeInForce::ImmediateOrCancel,
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|