Skip to main content

quant_primitives/
position_config.rs

1//! Position configuration for strategy sizing.
2//!
3//! `PositionConfig` is a value object that defines how a strategy
4//! sizes its positions. Used by `PositionSizer` to convert signals
5//! into concrete order quantities.
6
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9
10/// Configuration for position sizing within a strategy.
11///
12/// # Invariants
13///
14/// - `max_position_pct` must be in `[0.0, 1.0]`
15/// - `risk_per_trade_pct` (if present) must be in `[0.0, 1.0]`
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct PositionConfig {
18    max_position_pct: Decimal,
19    volatility_scaling: bool,
20    risk_per_trade_pct: Option<Decimal>,
21}
22
23/// Error for invalid `PositionConfig` construction.
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25pub enum PositionConfigError {
26    /// max_position_pct is outside [0, 1].
27    #[error("max_position_pct {0} out of range [0, 1]")]
28    MaxPositionOutOfRange(Decimal),
29    /// risk_per_trade_pct is outside [0, 1].
30    #[error("risk_per_trade_pct {0} out of range [0, 1]")]
31    RiskPerTradeOutOfRange(Decimal),
32}
33
34impl PositionConfig {
35    /// Create a new position config, validating all fields.
36    pub fn new(
37        max_position_pct: Decimal,
38        volatility_scaling: bool,
39        risk_per_trade_pct: Option<Decimal>,
40    ) -> Result<Self, PositionConfigError> {
41        if max_position_pct < Decimal::ZERO || max_position_pct > Decimal::ONE {
42            return Err(PositionConfigError::MaxPositionOutOfRange(max_position_pct));
43        }
44        if let Some(risk) = risk_per_trade_pct {
45            if risk < Decimal::ZERO || risk > Decimal::ONE {
46                return Err(PositionConfigError::RiskPerTradeOutOfRange(risk));
47            }
48        }
49        Ok(Self {
50            max_position_pct,
51            volatility_scaling,
52            risk_per_trade_pct,
53        })
54    }
55
56    /// Maximum position as a fraction of equity (e.g., 0.1 = 10%).
57    pub fn max_position_pct(&self) -> Decimal {
58        self.max_position_pct
59    }
60
61    /// Whether to scale position size by inverse volatility.
62    pub fn volatility_scaling(&self) -> bool {
63        self.volatility_scaling
64    }
65
66    /// Risk per trade as a fraction of equity, if set.
67    pub fn risk_per_trade_pct(&self) -> Option<Decimal> {
68        self.risk_per_trade_pct
69    }
70}
71
72impl Default for PositionConfig {
73    fn default() -> Self {
74        Self {
75            max_position_pct: Decimal::new(1, 1), // 0.1 = 10%
76            volatility_scaling: false,
77            risk_per_trade_pct: None,
78        }
79    }
80}
81
82#[cfg(test)]
83#[path = "position_config_tests.rs"]
84mod tests;