quant-primitives 0.7.0

Pure trading primitives — candles, intervals, symbols, currencies, asset taxonomy
Documentation
//! Position configuration for strategy sizing.
//!
//! `PositionConfig` is a value object that defines how a strategy
//! sizes its positions. Used by `PositionSizer` to convert signals
//! into concrete order quantities.

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

/// Configuration for position sizing within a strategy.
///
/// # Invariants
///
/// - `max_position_pct` must be in `[0.0, 1.0]`
/// - `risk_per_trade_pct` (if present) must be in `[0.0, 1.0]`
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PositionConfig {
    max_position_pct: Decimal,
    volatility_scaling: bool,
    risk_per_trade_pct: Option<Decimal>,
}

/// Error for invalid `PositionConfig` construction.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PositionConfigError {
    /// max_position_pct is outside [0, 1].
    #[error("max_position_pct {0} out of range [0, 1]")]
    MaxPositionOutOfRange(Decimal),
    /// risk_per_trade_pct is outside [0, 1].
    #[error("risk_per_trade_pct {0} out of range [0, 1]")]
    RiskPerTradeOutOfRange(Decimal),
}

impl PositionConfig {
    /// Create a new position config, validating all fields.
    pub fn new(
        max_position_pct: Decimal,
        volatility_scaling: bool,
        risk_per_trade_pct: Option<Decimal>,
    ) -> Result<Self, PositionConfigError> {
        if max_position_pct < Decimal::ZERO || max_position_pct > Decimal::ONE {
            return Err(PositionConfigError::MaxPositionOutOfRange(max_position_pct));
        }
        if let Some(risk) = risk_per_trade_pct {
            if risk < Decimal::ZERO || risk > Decimal::ONE {
                return Err(PositionConfigError::RiskPerTradeOutOfRange(risk));
            }
        }
        Ok(Self {
            max_position_pct,
            volatility_scaling,
            risk_per_trade_pct,
        })
    }

    /// Maximum position as a fraction of equity (e.g., 0.1 = 10%).
    pub fn max_position_pct(&self) -> Decimal {
        self.max_position_pct
    }

    /// Whether to scale position size by inverse volatility.
    pub fn volatility_scaling(&self) -> bool {
        self.volatility_scaling
    }

    /// Risk per trade as a fraction of equity, if set.
    pub fn risk_per_trade_pct(&self) -> Option<Decimal> {
        self.risk_per_trade_pct
    }
}

impl Default for PositionConfig {
    fn default() -> Self {
        Self {
            max_position_pct: Decimal::new(1, 1), // 0.1 = 10%
            volatility_scaling: false,
            risk_per_trade_pct: None,
        }
    }
}

#[cfg(test)]
#[path = "position_config_tests.rs"]
mod tests;