1pub const ABS_TOLERANCE: f64 = 1.0e-9;
4pub const REL_TOLERANCE: f64 = 1.0e-12;
5
6#[derive(Clone, Debug, PartialEq)]
7pub struct Fixture {
8 pub open: Vec<f64>,
9 pub high: Vec<f64>,
10 pub low: Vec<f64>,
11 pub close: Vec<f64>,
12 pub volume: Vec<f64>,
13 pub auxiliary: Vec<f64>,
14}
15
16impl Fixture {
17 pub fn len(&self) -> usize {
18 self.close.len()
19 }
20
21 pub fn is_empty(&self) -> bool {
22 self.close.is_empty()
23 }
24
25 pub fn validate(&self) -> Result<(), String> {
26 let length = self.close.len();
27 for (name, values) in [
28 ("open", &self.open),
29 ("high", &self.high),
30 ("low", &self.low),
31 ("volume", &self.volume),
32 ("auxiliary", &self.auxiliary),
33 ] {
34 if values.len() != length {
35 return Err(format!(
36 "fixture {name} length {} differs from close length {length}",
37 values.len()
38 ));
39 }
40 }
41 for index in 0..length {
42 let values = [
43 self.open[index],
44 self.high[index],
45 self.low[index],
46 self.close[index],
47 self.volume[index],
48 self.auxiliary[index],
49 ];
50 if values.iter().any(|value| !value.is_finite()) {
51 return Err(format!(
52 "fixture contains a non-finite value at index {index}"
53 ));
54 }
55 if self.high[index] < self.open[index].max(self.close[index]) {
56 return Err(format!(
57 "fixture high violates OHLC invariants at index {index}"
58 ));
59 }
60 if self.low[index] > self.open[index].min(self.close[index]) {
61 return Err(format!(
62 "fixture low violates OHLC invariants at index {index}"
63 ));
64 }
65 if self.volume[index] < 0.0 {
66 return Err(format!("fixture volume is negative at index {index}"));
67 }
68 }
69 Ok(())
70 }
71}
72
73pub use crate::fixture::series_fixture;
74
75pub fn catalogue_fixture(size: usize) -> Fixture {
76 let close = series_fixture(size, 0);
77 let auxiliary = series_fixture(size, 2)
78 .into_iter()
79 .enumerate()
80 .map(|(index, value)| value * 0.75 + (index % 13) as f64 * 0.02)
81 .collect::<Vec<_>>();
82 let open = close
83 .iter()
84 .enumerate()
85 .map(|(index, value)| value + ((index % 9) as f64 - 4.0) * 0.035)
86 .collect::<Vec<_>>();
87 let high = open
88 .iter()
89 .zip(&close)
90 .enumerate()
91 .map(|(index, (open, close))| open.max(*close) + 0.5 + (index % 11) as f64 * 0.03)
92 .collect::<Vec<_>>();
93 let low = open
94 .iter()
95 .zip(&close)
96 .enumerate()
97 .map(|(index, (open, close))| open.min(*close) - 0.5 - (index % 7) as f64 * 0.025)
98 .collect::<Vec<_>>();
99 let volume = series_fixture(size, 1)
100 .into_iter()
101 .map(|value| 10_000.0 + value * 100.0)
102 .collect::<Vec<_>>();
103 Fixture {
104 open,
105 high,
106 low,
107 close,
108 volume,
109 auxiliary,
110 }
111}
112
113pub fn input_checksum(values: &[f64]) -> String {
114 let mut hash = 0xcbf29ce484222325_u64;
115 hash_f64s(&mut hash, values);
116 format!("fnv1a64:{hash:016x}")
117}
118
119pub fn fixture_checksum(fixture: &Fixture) -> String {
120 let mut hash = 0xcbf29ce484222325_u64;
121 for (name, values) in [
122 ("open", fixture.open.as_slice()),
123 ("high", fixture.high.as_slice()),
124 ("low", fixture.low.as_slice()),
125 ("close", fixture.close.as_slice()),
126 ("volume", fixture.volume.as_slice()),
127 ("auxiliary", fixture.auxiliary.as_slice()),
128 ] {
129 hash_bytes(&mut hash, name.as_bytes());
130 hash_bytes(&mut hash, &(values.len() as u64).to_le_bytes());
131 hash_f64s(&mut hash, values);
132 }
133 format!("fnv1a64:{hash:016x}")
134}
135
136fn hash_f64s(hash: &mut u64, values: &[f64]) {
137 for value in values {
138 hash_bytes(hash, &value.to_le_bytes());
139 }
140}
141
142fn hash_bytes(hash: &mut u64, bytes: &[u8]) {
143 for byte in bytes {
144 *hash ^= u64::from(*byte);
145 *hash = hash.wrapping_mul(0x100000001b3);
146 }
147}
148
149#[derive(Clone, Debug, PartialEq)]
150pub enum OutputValues {
151 Float(Vec<Vec<f64>>),
152 Integer(Vec<Vec<i32>>),
153}
154
155impl OutputValues {
156 pub fn kind(&self) -> &'static str {
157 match self {
158 Self::Float(_) => "float",
159 Self::Integer(_) => "integer",
160 }
161 }
162
163 pub fn arity(&self) -> usize {
164 match self {
165 Self::Float(columns) => columns.len(),
166 Self::Integer(columns) => columns.len(),
167 }
168 }
169
170 pub fn column_len(&self) -> Result<usize, String> {
171 let lengths = match self {
172 Self::Float(columns) => columns.iter().map(Vec::len).collect::<Vec<_>>(),
173 Self::Integer(columns) => columns.iter().map(Vec::len).collect::<Vec<_>>(),
174 };
175 let Some(&first) = lengths.first() else {
176 return Err("output has no columns".to_owned());
177 };
178 if lengths.iter().any(|length| *length != first) {
179 return Err(format!("output columns have unequal lengths: {lengths:?}"));
180 }
181 Ok(first)
182 }
183}
184
185#[derive(Clone, Debug, PartialEq)]
186pub struct VerifiedOutput {
187 pub begin: usize,
188 pub count: usize,
189 pub values: OutputValues,
190}
191
192impl VerifiedOutput {
193 pub fn validate_shape(&self) -> Result<(), String> {
194 let actual = self.values.column_len()?;
195 if actual != self.count {
196 return Err(format!(
197 "declared output count {} differs from column length {actual}",
198 self.count
199 ));
200 }
201 Ok(())
202 }
203
204 pub fn checksum(&self) -> String {
205 let mut hash = 0xcbf29ce484222325_u64;
206 hash_bytes(&mut hash, self.values.kind().as_bytes());
207 hash_bytes(&mut hash, &(self.begin as u64).to_le_bytes());
208 hash_bytes(&mut hash, &(self.count as u64).to_le_bytes());
209 match &self.values {
210 OutputValues::Float(columns) => {
211 for column in columns {
212 hash_f64s(&mut hash, column);
213 }
214 }
215 OutputValues::Integer(columns) => {
216 for column in columns {
217 for value in column {
218 hash_bytes(&mut hash, &value.to_le_bytes());
219 }
220 }
221 }
222 }
223 format!("fnv1a64:{hash:016x}")
224 }
225}
226
227pub fn validate_outputs(
228 expected: &VerifiedOutput,
229 implementation: &str,
230 actual: &VerifiedOutput,
231) -> Result<(), String> {
232 expected
233 .validate_shape()
234 .map_err(|error| format!("reference output shape: {error}"))?;
235 actual
236 .validate_shape()
237 .map_err(|error| format!("{implementation} output shape: {error}"))?;
238 if (actual.begin, actual.count) != (expected.begin, expected.count) {
239 return Err(format!(
240 "{implementation} OutputRange mismatch: expected begin {} count {}, got begin {} count {}",
241 expected.begin, expected.count, actual.begin, actual.count
242 ));
243 }
244 if actual.values.kind() != expected.values.kind() {
245 return Err(format!(
246 "{implementation} output kind mismatch: expected {}, got {}",
247 expected.values.kind(),
248 actual.values.kind()
249 ));
250 }
251 if actual.values.arity() != expected.values.arity() {
252 return Err(format!(
253 "{implementation} output arity mismatch: expected {}, got {}",
254 expected.values.arity(),
255 actual.values.arity()
256 ));
257 }
258 match (&expected.values, &actual.values) {
259 (OutputValues::Float(expected_columns), OutputValues::Float(actual_columns)) => {
260 for (column, (expected_values, actual_values)) in
261 expected_columns.iter().zip(actual_columns).enumerate()
262 {
263 for (index, (&expected_value, &actual_value)) in
264 expected_values.iter().zip(actual_values).enumerate()
265 {
266 if !expected_value.is_finite() || !actual_value.is_finite() {
267 if expected_value.to_bits() != actual_value.to_bits() {
268 return Err(format!("{implementation} non-finite placement mismatch at column {column} compact index {index}: expected {expected_value:?}, got {actual_value:?}"));
269 }
270 continue;
271 }
272 let difference = (actual_value - expected_value).abs();
273 let tolerance = ABS_TOLERANCE.max(REL_TOLERANCE * expected_value.abs());
274 if difference > tolerance {
275 return Err(format!("{implementation} value mismatch at column {column} compact index {index}: expected {expected_value:.17e}, got {actual_value:.17e}, difference {difference:.3e}, tolerance {tolerance:.3e}"));
276 }
277 }
278 }
279 }
280 (OutputValues::Integer(expected_columns), OutputValues::Integer(actual_columns)) => {
281 for (column, (expected_values, actual_values)) in
282 expected_columns.iter().zip(actual_columns).enumerate()
283 {
284 for (index, (&expected_value, &actual_value)) in
285 expected_values.iter().zip(actual_values).enumerate()
286 {
287 if actual_value != expected_value {
288 return Err(format!("{implementation} exact integer mismatch at column {column} compact index {index}: expected {expected_value}, got {actual_value}"));
289 }
290 }
291 }
292 }
293 _ => unreachable!("output kinds were checked above"),
294 }
295 Ok(())
296}