Files
foxhunt/crates/ml-ppo/src/position_limits.rs
jgrusewski 7ef92983f9 fix(clippy): apply cargo clippy --fix across workspace
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>
2026-03-10 11:17:51 +01:00

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
}
}