tulip_rs 0.1.4

High-performance technical analysis library — 100+ indicators and 60+ candlestick patterns with SIMD acceleration
Documentation
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
#[cfg(feature = "simd_assets")]
pub use crate::indicators::simd_indicators::by_asset::ultosc::indicator_by_assets;

#[cfg(feature = "simd_options")]
pub use crate::indicators::simd_indicators::by_option::ultosc::indicator_by_options;

pub mod import {
    //! Internal imports and constants shared by the [`assets`] and [`options`] SIMD
    //! sub-modules for the Ultimate Oscillator (ULTOSC) indicator.
    pub(crate) use crate::indicators::simd_indicators::simd_types::F64Constants;
    pub(crate) use crate::indicators::ultosc::State;
    pub(crate) use crate::ring_buffer::multi_buffer::multi_buffer::{MultiBuffer, RingBuffer};
    pub(crate) use std::simd::{num::SimdFloat, Simd};
    pub(crate) struct UltoscF64Constants<const N: usize>;

    impl<const N: usize> UltoscF64Constants<N> {
        pub const DIV: Simd<f64, N> = Simd::splat(100.0 / 7.0);
    }
}

pub mod assets {
    //! Per-asset SIMD state and compute for the Ultimate Oscillator (ULTOSC) indicator.
    use super::import::*;
    pub(crate) use crate::ring_buffer::multi_buffer::multi_buffer::SimdRingBuffer;
    /// SIMD-parallel state for the Ultimate Oscillator (ULTOSC) indicator, holding `N` lanes of
    /// per-asset state.
    pub struct SimdState<const N: usize> {
        buffer: MultiBuffer<2, Simd<f64, N>>,

        bp_short_sum: Simd<f64, N>,
        bp_medium_sum: Simd<f64, N>,
        bp_long_sum: Simd<f64, N>,

        tr_short_sum: Simd<f64, N>,
        tr_medium_sum: Simd<f64, N>,
        tr_long_sum: Simd<f64, N>,

        prev_close: Simd<f64, N>,
    }

    impl<const N: usize> SimdState<N> {
        /// Constructs a [`SimdState`] by interleaving the fields of `N` scalar [`State`] references
        /// into SIMD lanes.
        pub fn new(states: &mut [&mut State]) -> Self {
            debug_assert_eq!(states.len(), N, "Number of states must match SIMD width");

            let buffer_refs: [&MultiBuffer<2, f64>; N] =
                core::array::from_fn(|i| &states[i].buffer);
            let buffer = <MultiBuffer<2, Simd<f64, N>> as SimdRingBuffer<2, N>>::from_f64_buffers(
                buffer_refs,
            );

            let mut bp_short_sum = [0.0; N];
            let mut bp_medium_sum = [0.0; N];
            let mut bp_long_sum = [0.0; N];

            let mut tr_short_sum = [0.0; N];
            let mut tr_medium_sum = [0.0; N];
            let mut tr_long_sum = [0.0; N];

            let mut prev_close = [0.0; N];

            for (i, state) in states.iter_mut().enumerate() {
                (bp_short_sum[i], bp_medium_sum[i], bp_long_sum[i]) =
                    (state.bp_sums_2x[0], state.bp_sums_2x[1], state.bp_long_sum);
                (tr_short_sum[i], tr_medium_sum[i], tr_long_sum[i]) =
                    (state.tr_sums_2x[0], state.tr_sums_2x[1], state.tr_long_sum);
                prev_close[i] = state.prev_close;
            }

            Self {
                buffer,
                bp_short_sum: Simd::from_array(bp_short_sum),
                bp_medium_sum: Simd::from_array(bp_medium_sum),
                bp_long_sum: Simd::from_array(bp_long_sum),
                tr_short_sum: Simd::from_array(tr_short_sum),
                tr_medium_sum: Simd::from_array(tr_medium_sum),
                tr_long_sum: Simd::from_array(tr_long_sum),
                prev_close: Simd::from_array(prev_close),
            }
        }

        /// Converts this SIMD state into an owned array of `N` scalar [`State`] values.
        pub fn to_states(&self) -> [State; N] {
            let buffers = self.buffer.to_f64_buffers();
            let bp_short_sum = self.bp_short_sum.to_array();
            let bp_medium_sum = self.bp_medium_sum.to_array();
            let bp_long_sum = self.bp_long_sum.to_array();
            let tr_short_sum = self.tr_short_sum.to_array();
            let tr_medium_sum = self.tr_medium_sum.to_array();
            let tr_long_sum = self.tr_long_sum.to_array();
            let prev_close = self.prev_close.to_array();
            // Use into_iter() to consume the arrays and avoid move issues
            let mut states_vec = Vec::<State>::with_capacity(N);
            for (i, buffer) in buffers.into_iter().enumerate() {
                states_vec.push(State {
                    buffer,
                    bp_long_sum: bp_long_sum[i],
                    bp_sums_2x: Simd::<f64, 2>::from_array([bp_short_sum[i], bp_medium_sum[i]]),
                    tr_long_sum: tr_long_sum[i],
                    tr_sums_2x: Simd::<f64, 2>::from_array([tr_short_sum[i], tr_medium_sum[i]]),
                    prev_close: prev_close[i],
                });
            }

            // Convert Vec to array
            states_vec
                .try_into()
                .unwrap_or_else(|_| panic!("Failed to convert states_vec to array"))
        }

        /// Writes SIMD state back into `N` scalar [`State`] references.
        pub fn write_states(&self, states: &mut [&mut State]) {
            // First, handle the buffer updates
            let buffers = self.buffer.to_f64_buffers();
            let bp_short_sum = self.bp_short_sum.to_array();
            let bp_medium_sum = self.bp_medium_sum.to_array();
            let bp_long_sum = self.bp_long_sum.to_array();
            let tr_short_sum = self.tr_short_sum.to_array();
            let tr_medium_sum = self.tr_medium_sum.to_array();
            let tr_long_sum = self.tr_long_sum.to_array();
            let prev_close = self.prev_close.to_array();

            for (i, buffer) in buffers.into_iter().enumerate() {
                states[i].buffer = buffer;
                (
                    states[i].bp_sums_2x[0],
                    states[i].bp_sums_2x[1],
                    states[i].bp_long_sum,
                ) = (bp_short_sum[i], bp_medium_sum[i], bp_long_sum[i]);
                (
                    states[i].tr_sums_2x[0],
                    states[i].tr_sums_2x[1],
                    states[i].tr_long_sum,
                ) = (tr_short_sum[i], tr_medium_sum[i], tr_long_sum[i]);
                states[i].prev_close = prev_close[i];
            }
        }

        /// Computes one bar of the Ultimate Oscillator for `N` assets simultaneously.
        ///
        /// Updates the rolling buying-pressure and true-range sums for the short, medium, and long
        /// periods, then returns the weighted oscillator value for each lane. Returns `0.0` lanes
        /// until the long-period buffer is full.
        ///
        /// # Arguments
        ///
        /// * `high` - High prices for this bar.
        /// * `low` - Low prices for this bar.
        /// * `close` - Close prices for this bar.
        /// * `periods` - Tuple `(short_period, medium_period)` for the shorter rolling windows.
        ///
        /// # Returns
        ///
        /// ULTOSC values in the range `[0, 100]` for all `N` lanes, or `0.0` while warming up.
        #[inline(always)]
        pub fn calc(
            &mut self,
            high: Simd<f64, N>,
            low: Simd<f64, N>,
            close: Simd<f64, N>,
            periods: (usize, usize),
        ) -> Simd<f64, N> {
            let (short_period, medium_period) = periods;

            let true_low = low.simd_min(self.prev_close);
            let true_high = high.simd_max(self.prev_close);
            let bp = close - true_low;
            let tr = true_high - true_low;

            if let Some(old) = self.buffer.push_with_info([bp, tr]) {
                self.bp_long_sum += bp - old[0];
                self.tr_long_sum += tr - old[1];
            } else {
                self.bp_long_sum += bp;
                self.tr_long_sum += tr;
            }
            let [[bp_short, bp_medium], [tr_short, tr_medium]] = self
                .buffer
                .get_by_periods::<2>([short_period, medium_period]);
            self.bp_short_sum += bp - bp_short;
            self.bp_medium_sum += bp - bp_medium;
            self.tr_short_sum += tr - tr_short;
            self.tr_medium_sum += tr - tr_medium;

            self.prev_close = close;

            if self.buffer.is_full() {
                let first = F64Constants::FOUR * (self.bp_short_sum / self.tr_short_sum);
                let second = F64Constants::TWO * (self.bp_medium_sum / self.tr_medium_sum);
                let third = self.bp_long_sum / self.tr_long_sum;
                return (first + second + third) * UltoscF64Constants::DIV;
            }
            F64Constants::ZERO
        }
        /// Unchecked variant of [`calc`](SimdState::calc) that assumes the long-period ring buffer
        /// is already full.
        ///
        /// # Arguments
        ///
        /// * `high` - High prices for this bar.
        /// * `low` - Low prices for this bar.
        /// * `close` - Close prices for this bar.
        /// * `periods` - Tuple `(short_period, medium_period)` for the shorter rolling windows.
        ///
        /// # Returns
        ///
        /// ULTOSC values for all `N` lanes.
        ///
        /// # Safety
        ///
        /// The internal ring buffer must be fully initialised (i.e., at least `long_period` bars
        /// have been processed) before calling this function. Calling it on an uninitialised buffer
        /// will produce incorrect results or undefined behaviour.
        #[inline(always)]
        pub unsafe fn calc_unchecked(
            &mut self,
            high: &Simd<f64, N>,
            low: &Simd<f64, N>,
            close: &Simd<f64, N>,
            periods: (usize, usize),
        ) -> Simd<f64, N> {
            let (short_period, medium_period) = periods;
            let true_low = low.simd_min(self.prev_close);
            let true_high = high.simd_max(self.prev_close);
            let bp = close - true_low;
            let tr = true_high - true_low;

            let old = self.buffer.push_with_info_unchecked([bp, tr]);
            self.bp_long_sum += bp - old[0];
            self.tr_long_sum += tr - old[1];

            let [[bp_short, bp_medium], [tr_short, tr_medium]] = self
                .buffer
                .get_by_periods::<2>([short_period, medium_period]);
            self.bp_short_sum += bp - bp_short;
            self.bp_medium_sum += bp - bp_medium;
            self.tr_short_sum += tr - tr_short;
            self.tr_medium_sum += tr - tr_medium;

            self.prev_close = *close;

            let first = F64Constants::FOUR * (self.bp_short_sum / self.tr_short_sum);
            let second = F64Constants::TWO * (self.bp_medium_sum / self.tr_medium_sum);
            let third = self.bp_long_sum / self.tr_long_sum;

            (first + second + third) * UltoscF64Constants::DIV
        }
    }
}

pub mod options {
    //! Per-option SIMD state and compute for the Ultimate Oscillator (ULTOSC) indicator.
    use super::import::*;
    /// SIMD-parallel state for the Ultimate Oscillator (ULTOSC) indicator, holding `N` lanes of
    /// per-option state.
    pub struct SimdState<const N: usize> {
        buffer: MultiBuffer<2>,
        periods: ([usize; N], [usize; N], [usize; N]),
        bp_short_sum: Simd<f64, N>,
        bp_medium_sum: Simd<f64, N>,
        bp_long_sum: Simd<f64, N>,

        tr_short_sum: Simd<f64, N>,
        tr_medium_sum: Simd<f64, N>,
        tr_long_sum: Simd<f64, N>,

        prev_close: f64,
    }

    impl<const N: usize> SimdState<N> {
        /// Constructs a [`SimdState`] from `N` scalar [`State`] references, one per option-set lane.
        ///
        /// Selects the largest-capacity buffer as the shared ring buffer and interleaves the
        /// rolling sums into SIMD lanes.
        ///
        /// # Arguments
        ///
        /// * `states` - Mutable references to `N` scalar states (one per option set).
        /// * `periods` - Arrays of `(short_period, medium_period, long_period)` for each lane.
        pub fn new(
            states: &mut [&mut State],
            periods: ([usize; N], [usize; N], [usize; N]),
        ) -> Self {
            debug_assert_eq!(states.len(), N, "Number of states must match SIMD width");
            let mut main_buffer = 0;
            for i in 1..N {
                if states[main_buffer].buffer.capacity < states[i].buffer.capacity {
                    main_buffer = i;
                }
            }
            let buffer = states[main_buffer].buffer.clone();

            let mut bp_short_sum = [0.0; N];
            let mut bp_medium_sum = [0.0; N];
            let mut bp_long_sum = [0.0; N];

            let mut tr_short_sum = [0.0; N];
            let mut tr_medium_sum = [0.0; N];
            let mut tr_long_sum = [0.0; N];

            let prev_close = states[main_buffer].prev_close;

            for (i, state) in states.iter_mut().enumerate() {
                (bp_short_sum[i], bp_medium_sum[i], bp_long_sum[i]) =
                    (state.bp_sums_2x[0], state.bp_sums_2x[1], state.bp_long_sum);
                (tr_short_sum[i], tr_medium_sum[i], tr_long_sum[i]) =
                    (state.tr_sums_2x[0], state.tr_sums_2x[1], state.tr_long_sum);
            }

            Self {
                buffer,
                bp_short_sum: Simd::from_array(bp_short_sum),
                bp_medium_sum: Simd::from_array(bp_medium_sum),
                bp_long_sum: Simd::from_array(bp_long_sum),
                tr_short_sum: Simd::from_array(tr_short_sum),
                tr_medium_sum: Simd::from_array(tr_medium_sum),
                tr_long_sum: Simd::from_array(tr_long_sum),
                prev_close,
                periods,
            }
        }

        /// Writes SIMD state back into `N` scalar [`State`] references, one per option-set lane.
        pub fn write_states(&self, states: &mut [&mut State]) {
            // First, handle the buffer updates
            let vals: [[Vec<f64>; 2]; N] =
                std::array::from_fn(|i| self.buffer.to_ordered_by_period(self.periods.2[i]));

            let bp_short_sum = self.bp_short_sum.to_array();
            let bp_medium_sum = self.bp_medium_sum.to_array();
            let bp_long_sum = self.bp_long_sum.to_array();
            let tr_short_sum = self.tr_short_sum.to_array();
            let tr_medium_sum = self.tr_medium_sum.to_array();
            let tr_long_sum = self.tr_long_sum.to_array();

            for (i, vals) in vals.into_iter().enumerate() {
                states[i].buffer = {
                    let len = vals[0].len();
                    MultiBuffer {
                        vals,
                        index: 0,
                        prev_idx: len - 1,
                        count: len,
                        capacity: len,
                    }
                };

                (
                    states[i].bp_sums_2x[0],
                    states[i].bp_sums_2x[1],
                    states[i].bp_long_sum,
                ) = (bp_short_sum[i], bp_medium_sum[i], bp_long_sum[i]);
                (
                    states[i].tr_sums_2x[0],
                    states[i].tr_sums_2x[1],
                    states[i].tr_long_sum,
                ) = (tr_short_sum[i], tr_medium_sum[i], tr_long_sum[i]);
                states[i].prev_close = self.prev_close;
            }
        }

        /*#[inline(always)]
        pub fn calc(
            &mut self,
            high: f64,
            low: f64,
            close: f64,
        ) -> Simd<f64, N> {
            let (short_period, medium_period) = periods;

            let true_low = low.min(self.prev_close);
            let true_high = high.max(self.prev_close);
            let bp = close - true_low;
            let tr = true_high - true_low;

            if let Some(old) = self.buffer.push_with_info([bp, tr]) {
                self.bp_long_sum += bp - old[0];
                self.tr_long_sum += tr - old[1];
            } else {
                self.bp_long_sum += bp;
                self.tr_long_sum += tr;
            }
            let [[bp_short, bp_medium], [tr_short, tr_medium]] = self
                .buffer
                .get_by_periods::<2>([short_period, medium_period]);
            self.bp_short_sum += bp - bp_short;
            self.bp_medium_sum += bp - bp_medium;
            self.tr_short_sum += tr - tr_short;
            self.tr_medium_sum += tr - tr_medium;

            self.prev_close = close;

            if self.buffer.is_full() {
                let first = F64Constants::FOUR * (self.bp_short_sum / self.tr_short_sum);
                let second = F64Constants::TWO * (self.bp_medium_sum / self.tr_medium_sum);
                let third = self.bp_long_sum / self.tr_long_sum;
                return (first + second + third) * UltoscF64Constants::DIV;
            }
            F64Constants::ZERO
        }*/
        /// Unchecked SIMD variant that computes one ULTOSC bar for `N` option-set lanes simultaneously.
        ///
        /// Accepts scalar `high`, `low`, `close` inputs (shared across all option lanes) and returns
        /// a SIMD vector of ULTOSC values, one per option-set lane.
        ///
        /// # Arguments
        ///
        /// * `high` - High price for this bar (scalar, shared across lanes).
        /// * `low` - Low price for this bar (scalar, shared across lanes).
        /// * `close` - Close price for this bar (scalar, shared across lanes).
        ///
        /// # Returns
        ///
        /// ULTOSC values for all `N` option-set lanes.
        ///
        /// # Safety
        ///
        /// The internal ring buffer must be fully initialised (i.e., at least `max(long_period)`
        /// bars have been processed) before calling this function.
        #[inline(always)]
        pub unsafe fn calc_unchecked(&mut self, high: f64, low: f64, close: f64) -> Simd<f64, N> {
            let (short_period, medium_period, long_period) = self.periods;
            let true_low = low.min(self.prev_close);
            let true_high = high.max(self.prev_close);
            let bp = close - true_low;
            let tr = true_high - true_low;

            let [bp_long_old, tr_long_old] = self
                .buffer
                .push_with_info_periods_unchecked([bp, tr], long_period);
            let bp = Simd::splat(bp);
            let tr = Simd::splat(tr);

            self.bp_long_sum += bp - Simd::from_array(bp_long_old);
            self.tr_long_sum += tr - Simd::from_array(tr_long_old);

            let [bp_medium_old, tr_medium_old] = self.buffer.get_by_periods(medium_period);
            self.bp_medium_sum += bp - Simd::from_array(bp_medium_old);
            self.tr_medium_sum += tr - Simd::from_array(tr_medium_old);

            let [bp_short_old, tr_short_old] = self.buffer.get_by_periods(short_period);
            self.bp_short_sum += bp - Simd::from_array(bp_short_old);
            self.tr_short_sum += tr - Simd::from_array(tr_short_old);

            self.prev_close = close;

            let first = F64Constants::FOUR * (self.bp_short_sum / self.tr_short_sum);
            let second = F64Constants::TWO * (self.bp_medium_sum / self.tr_medium_sum);
            let third = self.bp_long_sum / self.tr_long_sum;

            (first + second + third) * UltoscF64Constants::DIV
        }
    }
}