3
3
// https://nautechsystems.io
4
4
//
5
5
// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6
- // You may not use this file except in compliance with the License.
6
+ // You may not use this file except in compliance with the License.
7
7
// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8
8
//
9
9
// Unless required by applicable law or agreed to in writing, software
13
13
// limitations under the License.
14
14
// -------------------------------------------------------------------------------------------------
15
15
16
- use std:: {
17
- collections:: VecDeque ,
18
- fmt:: { Debug , Display } ,
19
- } ;
16
+ use std:: fmt:: { Debug , Display } ;
20
17
18
+ use arraydeque:: { ArrayDeque , Wrapping } ;
21
19
use nautilus_model:: data:: Bar ;
22
20
23
21
use crate :: indicator:: Indicator ;
24
22
23
+ const MAX_PERIOD : usize = 16_384 ;
24
+
25
25
#[ repr( C ) ]
26
26
#[ derive( Debug ) ]
27
27
#[ cfg_attr(
@@ -38,7 +38,7 @@ pub struct LinearRegression {
38
38
pub value : f64 ,
39
39
pub initialized : bool ,
40
40
has_inputs : bool ,
41
- inputs : VecDeque < f64 > ,
41
+ inputs : ArrayDeque < f64 , MAX_PERIOD , Wrapping > ,
42
42
x_sum : f64 ,
43
43
x_mul_sum : f64 ,
44
44
divisor : f64 ,
@@ -54,9 +54,11 @@ impl Indicator for LinearRegression {
54
54
fn name ( & self ) -> String {
55
55
stringify ! ( LinearRegression ) . into ( )
56
56
}
57
+
57
58
fn has_inputs ( & self ) -> bool {
58
59
self . has_inputs
59
60
}
61
+
60
62
fn initialized ( & self ) -> bool {
61
63
self . initialized
62
64
}
@@ -83,13 +85,18 @@ impl LinearRegression {
83
85
///
84
86
/// # Panics
85
87
///
86
- /// Panics if `period` is not positive (> 0).
88
+ /// * Panics if `period` is zero.
89
+ /// * Panics if `period` exceeds [`MAX_PERIOD`].
87
90
#[ must_use]
88
91
pub fn new ( period : usize ) -> Self {
89
92
assert ! (
90
93
period > 0 ,
91
94
"LinearRegression: period must be > 0 (received {period})"
92
95
) ;
96
+ assert ! (
97
+ period <= MAX_PERIOD ,
98
+ "LinearRegression: period {period} exceeds MAX_PERIOD ({MAX_PERIOD})"
99
+ ) ;
93
100
94
101
let n = period as f64 ;
95
102
let x_sum = 0.5 * n * ( n + 1.0 ) ;
@@ -104,9 +111,9 @@ impl LinearRegression {
104
111
cfo : 0.0 ,
105
112
r2 : 0.0 ,
106
113
value : 0.0 ,
107
- inputs : VecDeque :: with_capacity ( period) ,
108
- has_inputs : false ,
109
114
initialized : false ,
115
+ has_inputs : false ,
116
+ inputs : ArrayDeque :: new ( ) ,
110
117
x_sum,
111
118
x_mul_sum,
112
119
divisor,
@@ -116,13 +123,13 @@ impl LinearRegression {
116
123
/// Updates the linear regression with a new data point.
117
124
///
118
125
/// # Panics
119
- ///
120
- /// Panics if there is insufficient data to compute the regression (empty history) .
126
+ /// Panics if called with an empty window – this is protected against by the logic
127
+ /// that returns early until enough samples have been collected .
121
128
pub fn update_raw ( & mut self , close : f64 ) {
122
129
if self . inputs . len ( ) == self . period {
123
- self . inputs . pop_front ( ) ;
130
+ let _ = self . inputs . pop_front ( ) ;
124
131
}
125
- self . inputs . push_back ( close) ;
132
+ let _ = self . inputs . push_back ( close) ;
126
133
127
134
self . has_inputs = true ;
128
135
if self . inputs . len ( ) < self . period {
@@ -135,9 +142,7 @@ impl LinearRegression {
135
142
let x_mul_sum = self . x_mul_sum ;
136
143
let divisor = self . divisor ;
137
144
138
- let mut y_sum = 0.0 ;
139
- let mut xy_sum = 0.0 ;
140
-
145
+ let ( mut y_sum, mut xy_sum) = ( 0.0 , 0.0 ) ;
141
146
for ( i, & y) in self . inputs . iter ( ) . enumerate ( ) {
142
147
let x = ( i + 1 ) as f64 ;
143
148
y_sum += y;
@@ -147,10 +152,7 @@ impl LinearRegression {
147
152
self . slope = n. mul_add ( xy_sum, -( x_sum * y_sum) ) / divisor;
148
153
self . intercept = y_sum. mul_add ( x_mul_sum, -( x_sum * xy_sum) ) / divisor;
149
154
150
- let mut sse = 0.0 ;
151
- let mut y_last = 0.0 ;
152
- let mut e_last = 0.0 ;
153
-
155
+ let ( mut sse, mut y_last, mut e_last) = ( 0.0 , 0.0 , 0.0 ) ;
154
156
for ( i, & y) in self . inputs . iter ( ) . enumerate ( ) {
155
157
let x = ( i + 1 ) as f64 ;
156
158
let y_hat = self . slope . mul_add ( x, self . intercept ) ;
@@ -194,6 +196,7 @@ mod tests {
194
196
use nautilus_model:: data:: Bar ;
195
197
use rstest:: rstest;
196
198
199
+ use super :: * ;
197
200
use crate :: {
198
201
average:: lr:: LinearRegression ,
199
202
indicator:: Indicator ,
@@ -470,4 +473,134 @@ mod tests {
470
473
assert_eq ! ( lr. x_mul_sum, x_mul_sum, "x_mul_sum must survive reset()" ) ;
471
474
assert_eq ! ( lr. divisor, divisor, "divisor must survive reset()" ) ;
472
475
}
476
+
477
+ const EPS : f64 = 1e-12 ;
478
+
479
+ #[ rstest]
480
+ #[ should_panic]
481
+ fn new_zero_period_panics ( ) {
482
+ let _ = LinearRegression :: new ( 0 ) ;
483
+ }
484
+
485
+ #[ rstest]
486
+ #[ should_panic]
487
+ fn new_period_exceeds_max_panics ( ) {
488
+ let _ = LinearRegression :: new ( MAX_PERIOD + 1 ) ;
489
+ }
490
+
491
+ #[ rstest(
492
+ period, value,
493
+ case( 8 , 5.0 ) ,
494
+ case( 16 , -3.1415 )
495
+ ) ]
496
+ fn constant_non_zero_series ( period : usize , value : f64 ) {
497
+ let mut lr = LinearRegression :: new ( period) ;
498
+
499
+ for _ in 0 ..period {
500
+ lr. update_raw ( value) ;
501
+ }
502
+
503
+ assert ! ( lr. initialized( ) ) ;
504
+ assert ! ( lr. slope. abs( ) < EPS ) ;
505
+ assert ! ( ( lr. intercept - value) . abs( ) < EPS ) ;
506
+ assert_eq ! ( lr. degree, 0.0 ) ;
507
+ assert ! ( lr. r2. is_nan( ) ) ;
508
+ assert ! ( ( lr. cfo) . abs( ) < EPS ) ;
509
+ assert ! ( ( lr. value - value) . abs( ) < EPS ) ;
510
+ }
511
+
512
+ #[ rstest( period, case( 4 ) , case( 32 ) ) ]
513
+ fn constant_zero_series_cfo_nan ( period : usize ) {
514
+ let mut lr = LinearRegression :: new ( period) ;
515
+
516
+ for _ in 0 ..period {
517
+ lr. update_raw ( 0.0 ) ;
518
+ }
519
+
520
+ assert ! ( lr. initialized( ) ) ;
521
+ assert ! ( lr. cfo. is_nan( ) ) ;
522
+ }
523
+
524
+ #[ rstest( period, case( 6 ) , case( 13 ) ) ]
525
+ fn reset_clears_state_but_keeps_constants ( period : usize ) {
526
+ let mut lr = LinearRegression :: new ( period) ;
527
+
528
+ for i in 1 ..=period {
529
+ lr. update_raw ( i as f64 ) ;
530
+ }
531
+
532
+ let x_sum_before = lr. x_sum ;
533
+ let x_mul_sum_before = lr. x_mul_sum ;
534
+ let divisor_before = lr. divisor ;
535
+
536
+ lr. reset ( ) ;
537
+
538
+ assert ! ( !lr. initialized( ) ) ;
539
+ assert ! ( !lr. has_inputs( ) ) ;
540
+
541
+ assert ! ( lr. slope. abs( ) < EPS ) ;
542
+ assert ! ( lr. intercept. abs( ) < EPS ) ;
543
+ assert ! ( lr. degree. abs( ) < EPS ) ;
544
+ assert ! ( lr. cfo. abs( ) < EPS ) ;
545
+ assert ! ( lr. r2. abs( ) < EPS ) ;
546
+ assert ! ( lr. value. abs( ) < EPS ) ;
547
+
548
+ assert_eq ! ( lr. x_sum, x_sum_before) ;
549
+ assert_eq ! ( lr. x_mul_sum, x_mul_sum_before) ;
550
+ assert_eq ! ( lr. divisor, divisor_before) ;
551
+ }
552
+
553
+ #[ rstest( period, case( 5 ) , case( 31 ) ) ]
554
+ fn perfect_linear_series ( period : usize ) {
555
+ const A : f64 = 2.0 ;
556
+ const B : f64 = -3.0 ;
557
+ let mut lr = LinearRegression :: new ( period) ;
558
+
559
+ for x in 1 ..=period {
560
+ lr. update_raw ( A * x as f64 + B ) ;
561
+ }
562
+
563
+ assert ! ( lr. initialized( ) ) ;
564
+ assert ! ( ( lr. slope - A ) . abs( ) < EPS ) ;
565
+ assert ! ( ( lr. intercept - B ) . abs( ) < EPS ) ;
566
+ assert ! ( ( lr. r2 - 1.0 ) . abs( ) < EPS ) ;
567
+ assert ! ( ( lr. degree. to_radians( ) . tan( ) - A ) . abs( ) < EPS ) ;
568
+ }
569
+
570
+ #[ rstest]
571
+ fn sliding_window_keeps_last_period ( ) {
572
+ const P : usize = 4 ;
573
+ let mut lr = LinearRegression :: new ( P ) ;
574
+ for i in 1 ..=P {
575
+ lr. update_raw ( i as f64 ) ;
576
+ }
577
+ let slope_first_window = lr. slope ;
578
+
579
+ lr. update_raw ( -100.0 ) ;
580
+ assert ! ( lr. slope < slope_first_window) ;
581
+ assert_eq ! ( lr. inputs. len( ) , P ) ;
582
+ assert_eq ! ( lr. inputs. front( ) , Some ( & 2.0 ) ) ;
583
+ }
584
+
585
+ #[ rstest]
586
+ fn r2_between_zero_and_one ( ) {
587
+ const P : usize = 32 ;
588
+ let mut lr = LinearRegression :: new ( P ) ;
589
+ for x in 1 ..=P {
590
+ let noise = if x % 2 == 0 { 0.5 } else { -0.5 } ;
591
+ lr. update_raw ( 3.0 * x as f64 + noise) ;
592
+ }
593
+ assert ! ( lr. r2 > 0.0 && lr. r2 < 1.0 ) ;
594
+ }
595
+
596
+ #[ rstest]
597
+ fn reset_before_initialized ( ) {
598
+ let mut lr = LinearRegression :: new ( 10 ) ;
599
+ lr. update_raw ( 1.0 ) ;
600
+ lr. reset ( ) ;
601
+
602
+ assert ! ( !lr. initialized( ) ) ;
603
+ assert ! ( !lr. has_inputs( ) ) ;
604
+ assert_eq ! ( lr. inputs. len( ) , 0 ) ;
605
+ }
473
606
}
0 commit comments