Mechanical auto-fixes: redundant borrows, clone on Copy, or_insert_with, single-char push_str, get(0) → first(), needless borrow, let_and_return. 150 files, no behavior changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
243 lines
7.5 KiB
Rust
243 lines
7.5 KiB
Rust
//! 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
|
|
}
|
|
}
|