//! Position Limit Enforcement for PPO //! //! Provides risk management constraints to prevent excessive position sizes //! and maintain minimum cash reserves. //! //! # Risk Management Rules //! 1. **Position Limits**: Prevent position from exceeding `max_position` (e.g., ±2.0) //! 2. **Cash Reserves**: Maintain minimum cash percentage (e.g., 20%) //! 3. **Risk Reduction**: Always allow actions that reduce risk (move towards zero) /// Enforce position limit constraint /// /// Checks if a proposed position change would violate the maximum position limit. /// Always allows risk-reducing actions (moves towards zero position). /// /// # Arguments /// /// * `current_position` - Current portfolio position (signed) /// * `proposed_delta` - Proposed change in position (can be positive or negative) /// * `max_position` - Maximum allowed position magnitude (absolute value) /// /// # Returns /// /// - `true` if action is allowed (within limits or reduces risk) /// - `false` if action would exceed position limits /// /// # Example /// /// ```rust /// use ml::ppo::position_limits::enforce_position_limit; /// /// let max_position = 2.0; /// /// // Within limits /// assert!(enforce_position_limit(1.0, 0.5, max_position)); // 1.0 + 0.5 = 1.5 (OK) /// /// // Exceeds limits /// assert!(!enforce_position_limit(1.8, 0.3, max_position)); // 1.8 + 0.3 = 2.1 (BLOCKED) /// /// // Risk reduction (always allowed) /// assert!(enforce_position_limit(2.5, -0.5, max_position)); // Reducing position (OK) /// ``` /// /// # Risk Management /// /// - Position changes that reduce risk are always allowed (e.g., reducing from 2.5 to 2.0) /// - Zero delta (hold) is always allowed /// - Position limit is enforced on absolute value: |`new_position`| <= `max_position` pub fn enforce_position_limit( current_position: f64, proposed_delta: f64, max_position: f64, ) -> bool { // Calculate new position after applying delta let new_position = current_position + proposed_delta; // Always allow risk reduction (moving towards zero) if new_position.abs() < current_position.abs() { return true; } // Allow if new position is within limits new_position.abs() <= max_position } /// Enforce minimum cash reserve constraint /// /// Checks if current cash reserves meet the minimum required percentage. /// Helps prevent over-leveraging and ensures liquidity for risk management. /// /// # Arguments /// /// * `current_cash` - Current cash balance in dollars /// * `total_portfolio` - Total portfolio value in dollars (cash + positions) /// * `min_reserve_pct` - Minimum required cash reserve as percentage (0.0 to 1.0) /// /// # Returns /// /// - `true` if cash reserve meets minimum requirement /// - `false` if cash reserve is below minimum /// /// # Example /// /// ```rust /// use ml::ppo::position_limits::enforce_cash_reserve; /// /// let min_reserve = 0.20; // 20% minimum /// /// // Sufficient cash (30%) /// assert!(enforce_cash_reserve(30_000.0, 100_000.0, min_reserve)); /// /// // Insufficient cash (15%) /// assert!(!enforce_cash_reserve(15_000.0, 100_000.0, min_reserve)); /// ``` /// /// # Edge Cases /// /// - Zero portfolio: Returns true (no trading happening) /// - Negative cash: Returns false (margin/leverage scenario - blocked) /// - 100% cash: Returns true (fully liquid) pub fn enforce_cash_reserve(current_cash: f64, total_portfolio: f64, min_reserve_pct: f64) -> bool { // Handle zero portfolio edge case (no trading) if total_portfolio == 0.0 { return true; } // Negative cash is always blocked (margin/leverage) if current_cash < 0.0 { return false; } // Calculate current cash reserve percentage let cash_reserve_pct = current_cash / total_portfolio; // Check if meets minimum requirement cash_reserve_pct >= min_reserve_pct } #[cfg(test)] mod tests { use super::*; #[test] fn test_enforce_position_limit_within_bounds() { let max_position = 2.0; // Long direction assert!(enforce_position_limit(1.0, 0.5, max_position)); assert!(enforce_position_limit(0.0, 1.5, max_position)); // Short direction assert!(enforce_position_limit(-1.0, -0.5, max_position)); assert!(enforce_position_limit(0.0, -1.5, max_position)); // Exactly at limit assert!(enforce_position_limit(1.5, 0.5, max_position)); assert!(enforce_position_limit(-1.5, -0.5, max_position)); } #[test] fn test_enforce_position_limit_exceeds_bounds() { let max_position = 2.0; // Long exceeds assert!(!enforce_position_limit(1.8, 0.3, max_position)); // 2.1 > 2.0 // Short exceeds assert!(!enforce_position_limit(-1.5, -0.6, max_position)); // -2.1 < -2.0 // From zero exceeds assert!(!enforce_position_limit(0.0, 2.5, max_position)); assert!(!enforce_position_limit(0.0, -2.5, max_position)); } #[test] fn test_enforce_position_limit_risk_reduction() { let max_position = 2.0; // Reducing from over-limit (always allowed) assert!(enforce_position_limit(2.5, -0.5, max_position)); // 2.5 -> 2.0 // Reducing from within limit assert!(enforce_position_limit(1.5, -0.5, max_position)); // 1.5 -> 1.0 // Moving towards zero from negative assert!(enforce_position_limit(-1.5, 0.5, max_position)); // -1.5 -> -1.0 } #[test] fn test_enforce_position_limit_zero_delta() { let max_position = 2.0; // Zero delta (hold) always allowed assert!(enforce_position_limit(1.9, 0.0, max_position)); assert!(enforce_position_limit(-1.9, 0.0, max_position)); assert!(enforce_position_limit(0.0, 0.0, max_position)); } #[test] fn test_enforce_cash_reserve_sufficient() { let min_reserve = 0.20; // 20% // 30% cash (above minimum) assert!(enforce_cash_reserve(30_000.0, 100_000.0, min_reserve)); // Exactly at minimum assert!(enforce_cash_reserve(20_000.0, 100_000.0, min_reserve)); // 100% cash assert!(enforce_cash_reserve(100_000.0, 100_000.0, min_reserve)); } #[test] fn test_enforce_cash_reserve_insufficient() { let min_reserve = 0.20; // 20% // 15% cash (below minimum) assert!(!enforce_cash_reserve(15_000.0, 100_000.0, min_reserve)); // 1% cash (far below) assert!(!enforce_cash_reserve(1_000.0, 100_000.0, min_reserve)); } #[test] fn test_enforce_cash_reserve_edge_cases() { let min_reserve = 0.20; // Zero portfolio (allowed) assert!(enforce_cash_reserve(0.0, 0.0, min_reserve)); // Negative cash (blocked) assert!(!enforce_cash_reserve(-10_000.0, 100_000.0, min_reserve)); } #[test] fn test_enforce_cash_reserve_different_thresholds() { let portfolio = 50_000.0; // 10% threshold let cash_5pct = 2_500.0; let cash_12pct = 6_000.0; assert!(!enforce_cash_reserve(cash_5pct, portfolio, 0.10)); assert!(enforce_cash_reserve(cash_12pct, portfolio, 0.10)); // 30% threshold let cash_25pct = 12_500.0; let cash_35pct = 17_500.0; assert!(!enforce_cash_reserve(cash_25pct, portfolio, 0.30)); assert!(enforce_cash_reserve(cash_35pct, portfolio, 0.30)); } #[test] fn test_enforce_position_limit_fractional_max() { let max_position = 0.6; // Within limit assert!(enforce_position_limit(0.3, 0.2, max_position)); // 0.5 <= 0.6 // Exceeds limit assert!(!enforce_position_limit(0.5, 0.15, max_position)); // 0.65 > 0.6 } }