Skip to main content

fin_primitives/
error.rs

1//! Error types for the fin-primitives crate.
2//!
3//! All errors are named, typed, and propagatable via `thiserror`.
4//! Every variant has at least one test that triggers it.
5
6use rust_decimal::Decimal;
7
8/// All errors that can occur in fin-primitives operations.
9#[derive(Debug, thiserror::Error)]
10pub enum FinError {
11    /// Symbol string was empty or contained whitespace.
12    #[error("Symbol '{0}' is invalid (empty or contains whitespace)")]
13    InvalidSymbol(String),
14
15    /// Price value was zero or negative.
16    #[error("Price must be positive, got {0}")]
17    InvalidPrice(Decimal),
18
19    /// Quantity value was negative.
20    #[error("Quantity must be non-negative, got {0}")]
21    InvalidQuantity(Decimal),
22
23    /// Order book delta arrived out of sequence.
24    #[error("Order book sequence mismatch: expected {expected}, got {got}")]
25    SequenceMismatch {
26        /// The next sequence number the book expected.
27        expected: u64,
28        /// The sequence number that was actually received.
29        got: u64,
30    },
31
32    /// Not enough resting liquidity to fill the requested quantity.
33    #[error("No liquidity available for requested quantity {0}")]
34    InsufficientLiquidity(Decimal),
35
36    /// OHLCV bar failed internal invariant check (high >= low, etc.).
37    #[error("OHLCV bar invariant violated: {0}")]
38    BarInvariant(String),
39
40    /// A signal has not accumulated enough bars to produce a value.
41    #[error("Signal '{name}' not ready (requires {required} periods, have {have})")]
42    SignalNotReady {
43        /// Name of the signal that is not ready.
44        name: String,
45        /// Number of bars required before the signal produces a value.
46        required: usize,
47        /// Number of bars seen so far.
48        have: usize,
49    },
50
51    /// Position lookup failed for the given symbol.
52    #[error("Position not found for symbol '{0}'")]
53    PositionNotFound(String),
54
55    /// Ledger cash balance insufficient to cover the fill cost.
56    #[error("Insufficient funds: need {need}, have {have}")]
57    InsufficientFunds {
58        /// Amount of cash required for the fill (cost + commission).
59        need: Decimal,
60        /// Current cash balance in the ledger.
61        have: Decimal,
62    },
63
64    /// Timeframe duration was zero or negative.
65    #[error("Timeframe duration must be positive")]
66    InvalidTimeframe,
67
68    /// A Decimal arithmetic operation overflowed.
69    #[error("Arithmetic overflow in financial calculation")]
70    ArithmeticOverflow,
71
72    /// Order book ended up with an inverted spread after a delta was applied.
73    #[error("Inverted spread: best_bid {best_bid} >= best_ask {best_ask}")]
74    InvertedSpread {
75        /// Best bid price at the time the spread inversion was detected.
76        best_bid: Decimal,
77        /// Best ask price at the time the spread inversion was detected.
78        best_ask: Decimal,
79    },
80
81    /// Indicator or aggregator period was zero; must be at least 1.
82    #[error("Period must be at least 1, got {0}")]
83    InvalidPeriod(usize),
84
85    /// General-purpose validation error for invalid inputs.
86    #[error("Invalid input: {0}")]
87    InvalidInput(String),
88}
89
90impl FinError {
91    /// Returns `true` if this error is a period validation error.
92    pub fn is_period_error(&self) -> bool {
93        matches!(self, FinError::InvalidPeriod(_))
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_is_period_error_true_for_invalid_period() {
103        let e = FinError::InvalidPeriod(0);
104        assert!(e.is_period_error());
105    }
106
107    #[test]
108    fn test_is_period_error_false_for_other_errors() {
109        let e = FinError::InvalidSymbol("".to_owned());
110        assert!(!e.is_period_error());
111    }
112
113    #[test]
114    fn test_invalid_input_error_message() {
115        let e = FinError::InvalidInput("bad value".to_owned());
116        assert!(e.to_string().contains("bad value"));
117    }
118}