1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//! Precision boundary between `FinancialValue` (Decimal) and `StatisticalValue` (f64).
//!
//! Every crossing between the two representations passes through one of the two
//! checked helpers in this module. Silent fallbacks (`d.to_f64().unwrap_or(0.0)`,
//! `Decimal::try_from(f).unwrap_or(Decimal::ONE)`) are forbidden by the
//! `specs/precision/financial-precision-boundary.md` published-language spec in
//! qbot-core — every caller must propagate the error or surface it.
//!
//! # When to use
//!
//! - `decimal_to_f64_checked`: entering a statistical kernel (covariance,
//! regression, HRP, z-score comparison). The kernel consumes `f64` because
//! `Decimal` lacks the required math (`sqrt`, `ln`, eigendecomposition).
//! - `f64_to_decimal_checked`: exiting a statistical kernel back into any
//! booked value (position size, hedge ratio, stop multiplier). The guard
//! ensures NaN / ±Inf — which every well-behaved estimator produces on
//! degenerate inputs — refuses to become a silent zero or one.
//!
//! The functions return [`Result`] because the boundary is the correct place
//! to raise a domain error: a covariance collapse, a regression that failed
//! to converge, a rate-limiter returning ∞ backoff — all legitimate events
//! the domain layer must decide how to handle. Converting them to arbitrary
//! defaults hides real failures.
//!
//! # Reference
//!
//! - Spec: `specs/precision/financial-precision-boundary.md` (qbot-core).
//! - Historical scars: `hedge_sizing.rs:109` (NaN β → 1:1 hedge),
//! `pair_simulation/cost.rs:34` (silent zero z-score).
use ;
use Decimal;
/// Classification of a rejected non-finite `f64` at a precision boundary.
/// Error returned by the precision-boundary conversion helpers.
///
/// Rich variants — never `bool`, never a sentinel value. Callers must decide
/// per-variant whether to propagate, substitute a documented default, or
/// abort the containing operation.
/// The canonical rounding scale for `f64 → Decimal` crossings.
///
/// Six decimal places is the reference convention established by the original
/// `hrp::decimal_from_f64` helper in qbot-core domain-indicators — deep enough
/// for regression coefficients and HRP weights, shallow enough that rounding
/// reliably cancels IEEE-754 representation noise.
pub const PRECISION_SCALE: u32 = 6;
/// Canonical `Decimal → f64` conversion at a precision boundary.
///
/// Used by any caller that must feed a Decimal into a statistical kernel
/// (regression, covariance, HRP, z-score math). Returns `Err(OutOfDomain)` if
/// the Decimal cannot be represented as an `f64` — in practice only extremely
/// large Decimals (> ~1.7e308) trigger this, but the guard exists so callers
/// never receive a silent Inf.
///
/// Forbidden forms this replaces:
/// ```ignore
/// let f = d.to_f64().unwrap_or(0.0); // silent zero — FORBIDDEN
/// let f = d.to_f64().unwrap(); // panic — FORBIDDEN
/// ```
///
/// # Example
/// ```
/// use quant_primitives::precision_boundary::decimal_to_f64_checked;
/// use rust_decimal_macros::dec;
///
/// let z_score = decimal_to_f64_checked(dec!(1.5)).unwrap();
/// assert!((z_score - 1.5).abs() < 1e-10);
/// ```
/// Canonical `f64 → Decimal` conversion at a precision boundary.
///
/// Validates finiteness, rejects NaN / ±Inf, rounds to [`PRECISION_SCALE`]
/// decimal places. Used by any caller that must feed a statistical output
/// back into a booked value (position size, hedge ratio, stop multiplier).
///
/// Forbidden forms this replaces:
/// ```ignore
/// let d = Decimal::try_from(f).unwrap_or(Decimal::ONE); // silent default — FORBIDDEN
/// let d = Decimal::from_f64(f).unwrap(); // panic — FORBIDDEN
/// ```
///
/// # Example
/// ```
/// use quant_primitives::precision_boundary::f64_to_decimal_checked;
/// use rust_decimal_macros::dec;
///
/// let beta = f64_to_decimal_checked(1.234567891).unwrap();
/// assert_eq!(beta, dec!(1.234568)); // rounded to 6dp
///
/// let err = f64_to_decimal_checked(f64::NAN).unwrap_err();
/// assert!(matches!(
/// err,
/// quant_primitives::precision_boundary::PrecisionBoundaryError::NonFiniteInput { .. },
/// ));
/// ```