@@ -88,31 +88,40 @@ impl Indicator for HullMovingAverage {
88
88
}
89
89
}
90
90
91
- fn _get_weights ( size : usize ) -> Vec < f64 > {
92
- let mut weights: Vec < f64 > = ( 1 ..=size) . map ( |x| x as f64 ) . collect ( ) ;
93
- let divisor: f64 = weights. iter ( ) . sum ( ) ;
94
- weights = weights. iter ( ) . map ( |x| x / divisor) . collect ( ) ;
95
- weights
91
+ fn get_weights ( size : usize ) -> Vec < f64 > {
92
+ let mut w: Vec < f64 > = ( 1 ..=size) . map ( |x| x as f64 ) . collect ( ) ;
93
+ let divisor: f64 = w. iter ( ) . sum ( ) ;
94
+ for v in & mut w {
95
+ * v /= divisor;
96
+ }
97
+ w
96
98
}
97
99
98
100
impl HullMovingAverage {
99
101
/// Creates a new [`HullMovingAverage`] instance.
102
+ ///
103
+ /// # Panics
104
+ ///
105
+ /// Panics if `period` is not positive (> 0).
100
106
#[ must_use]
101
107
pub fn new ( period : usize , price_type : Option < PriceType > ) -> Self {
102
- let period_halved = period / 2 ;
103
- let period_sqrt = ( period as f64 ) . sqrt ( ) as usize ;
108
+ assert ! (
109
+ period > 0 ,
110
+ "HullMovingAverage: period must be > 0 (received {period})"
111
+ ) ;
112
+
113
+ let half = usize:: max ( 1 , period / 2 ) ;
114
+ let root = usize:: max ( 1 , ( period as f64 ) . sqrt ( ) as usize ) ;
104
115
105
- let w1 = _get_weights ( period_halved) ;
106
- let w2 = _get_weights ( period) ;
107
- let w3 = _get_weights ( period_sqrt) ;
116
+ let pt = price_type. unwrap_or ( PriceType :: Last ) ;
108
117
109
- let ma1 = WeightedMovingAverage :: new ( period_halved , w1 , price_type ) ;
110
- let ma2 = WeightedMovingAverage :: new ( period, w2 , price_type ) ;
111
- let ma3 = WeightedMovingAverage :: new ( period_sqrt , w3 , price_type ) ;
118
+ let ma1 = WeightedMovingAverage :: new ( half , get_weights ( half ) , Some ( pt ) ) ;
119
+ let ma2 = WeightedMovingAverage :: new ( period, get_weights ( period ) , Some ( pt ) ) ;
120
+ let ma3 = WeightedMovingAverage :: new ( root , get_weights ( root ) , Some ( pt ) ) ;
112
121
113
122
Self {
114
123
period,
115
- price_type : price_type . unwrap_or ( PriceType :: Last ) ,
124
+ price_type : pt ,
116
125
value : 0.0 ,
117
126
count : 0 ,
118
127
has_inputs : false ,
@@ -158,7 +167,10 @@ impl MovingAverage for HullMovingAverage {
158
167
////////////////////////////////////////////////////////////////////////////////
159
168
#[ cfg( test) ]
160
169
mod tests {
161
- use nautilus_model:: data:: { Bar , QuoteTick , TradeTick } ;
170
+ use nautilus_model:: {
171
+ data:: { Bar , QuoteTick , TradeTick } ,
172
+ enums:: PriceType ,
173
+ } ;
162
174
use rstest:: rstest;
163
175
164
176
use crate :: {
@@ -256,4 +268,138 @@ mod tests {
256
268
assert ! ( !indicator_hma_10. has_inputs) ;
257
269
assert ! ( !indicator_hma_10. initialized) ;
258
270
}
271
+
272
+ #[ rstest]
273
+ #[ should_panic( expected = "HullMovingAverage: period must be > 0" ) ]
274
+ fn test_new_with_zero_period_panics ( ) {
275
+ let _ = HullMovingAverage :: new ( 0 , None ) ;
276
+ }
277
+
278
+ #[ rstest]
279
+ #[ case( 1 ) ]
280
+ #[ case( 5 ) ]
281
+ #[ case( 128 ) ]
282
+ #[ case( 10_000 ) ]
283
+ fn test_new_with_positive_period_constructs ( #[ case] period : usize ) {
284
+ let hma = HullMovingAverage :: new ( period, None ) ;
285
+ assert_eq ! ( hma. period, period) ;
286
+ assert_eq ! ( hma. count( ) , 0 ) ;
287
+ assert ! ( !hma. initialized( ) ) ;
288
+ }
289
+
290
+ #[ rstest]
291
+ #[ case( PriceType :: Bid ) ]
292
+ #[ case( PriceType :: Ask ) ]
293
+ #[ case( PriceType :: Last ) ]
294
+ fn test_price_type_propagates_to_inner_wmas ( #[ case] pt : PriceType ) {
295
+ let hma = HullMovingAverage :: new ( 10 , Some ( pt) ) ;
296
+ assert_eq ! ( hma. price_type, pt) ;
297
+ assert_eq ! ( hma. ma1. price_type, pt) ;
298
+ assert_eq ! ( hma. ma2. price_type, pt) ;
299
+ assert_eq ! ( hma. ma3. price_type, pt) ;
300
+ }
301
+
302
+ #[ rstest]
303
+ fn test_price_type_defaults_to_last ( ) {
304
+ let hma = HullMovingAverage :: new ( 10 , None ) ;
305
+ assert_eq ! ( hma. price_type, PriceType :: Last ) ;
306
+ assert_eq ! ( hma. ma1. price_type, PriceType :: Last ) ;
307
+ assert_eq ! ( hma. ma2. price_type, PriceType :: Last ) ;
308
+ assert_eq ! ( hma. ma3. price_type, PriceType :: Last ) ;
309
+ }
310
+
311
+ #[ rstest]
312
+ #[ case( 10.0 ) ]
313
+ #[ case( -5.5 ) ]
314
+ #[ case( 42.42 ) ]
315
+ #[ case( 0.0 ) ]
316
+ fn period_one_degenerates_to_price ( #[ case] price : f64 ) {
317
+ let mut hma = HullMovingAverage :: new ( 1 , None ) ;
318
+
319
+ for _ in 0 ..5 {
320
+ hma. update_raw ( price) ;
321
+ assert ! (
322
+ ( hma. value( ) - price) . abs( ) < f64 :: EPSILON ,
323
+ "HMA(1) should equal last price {price}, got {}" ,
324
+ hma. value( )
325
+ ) ;
326
+ assert ! ( hma. initialized( ) , "HMA(1) must initialise immediately" ) ;
327
+ }
328
+ }
329
+
330
+ #[ rstest]
331
+ #[ case( 3 , 123.456_f64 ) ]
332
+ #[ case( 13 , 0.001_f64 ) ]
333
+ fn constant_series_yields_constant_value ( #[ case] period : usize , #[ case] constant : f64 ) {
334
+ let mut hma = HullMovingAverage :: new ( period, None ) ;
335
+
336
+ for _ in 0 ..( period * 4 ) {
337
+ hma. update_raw ( constant) ;
338
+ assert ! (
339
+ ( hma. value( ) - constant) . abs( ) < 1e-12 ,
340
+ "Expected {constant}, got {}" ,
341
+ hma. value( )
342
+ ) ;
343
+ }
344
+ assert ! ( hma. initialized( ) ) ;
345
+ }
346
+
347
+ #[ rstest]
348
+ fn alternating_extremes_bounded ( ) {
349
+ let mut hma = HullMovingAverage :: new ( 50 , None ) ;
350
+ let lows_highs = [ 0.0_f64 , 1_000.0_f64 ] ;
351
+
352
+ for i in 0 ..200 {
353
+ let price = lows_highs[ i & 1 ] ;
354
+ hma. update_raw ( price) ;
355
+
356
+ let v = hma. value ( ) ;
357
+ assert ! ( ( 0.0 ..=1_000.0 ) . contains( & v) , "HMA out of bounds: {v}" ) ;
358
+ }
359
+ }
360
+
361
+ #[ rstest]
362
+ #[ case( 2 ) ]
363
+ #[ case( 17 ) ]
364
+ #[ case( 128 ) ]
365
+ fn initialized_boundary ( #[ case] period : usize ) {
366
+ let mut hma = HullMovingAverage :: new ( period, None ) ;
367
+
368
+ for i in 0 ..( period - 1 ) {
369
+ hma. update_raw ( i as f64 ) ;
370
+ assert ! ( !hma. initialized( ) , "HMA wrongly initialised at count {i}" ) ;
371
+ }
372
+
373
+ hma. update_raw ( 0.0 ) ;
374
+ assert ! (
375
+ hma. initialized( ) ,
376
+ "HMA should initialise at exactly {period} ticks"
377
+ ) ;
378
+ }
379
+
380
+ #[ rstest]
381
+ #[ case( 2 ) ]
382
+ #[ case( 3 ) ]
383
+ fn small_periods_do_not_panic ( #[ case] period : usize ) {
384
+ let mut hma = HullMovingAverage :: new ( period, None ) ;
385
+ for i in 0 ..( period * 5 ) {
386
+ hma. update_raw ( i as f64 ) ;
387
+ }
388
+ assert ! ( hma. initialized( ) ) ;
389
+ }
390
+
391
+ #[ rstest]
392
+ fn negative_prices_supported ( ) {
393
+ let mut hma = HullMovingAverage :: new ( 10 , None ) ;
394
+ let prices = [ -5.0 , -4.0 , -3.0 , -2.5 , -2.0 , -1.5 , -1.0 , -0.5 , 0.0 , 0.5 ] ;
395
+
396
+ for & p in & prices {
397
+ hma. update_raw ( p) ;
398
+ let v = hma. value ( ) ;
399
+ assert ! (
400
+ v. is_finite( ) ,
401
+ "HMA produced a non-finite value {v} from negative prices"
402
+ ) ;
403
+ }
404
+ }
259
405
}
0 commit comments