@@ -3640,6 +3640,36 @@ def g(allow_non_overlapping: bool):
36403640 # Additional sanity check is that correlation should be 1 as middle ticks are ignored
36413641 np .testing .assert_allclose (res ["corr" ][1 ], np .array ([np .nan , 1.0 , 1.0 ]), equal_nan = True )
36423642
3643+ def test_ema_cov_horizon_bug (self ):
3644+ # Bug in finite horizon, adjusted, unbiased EMA covariance with ignore_na=True
3645+ # Also applies to ema_var/ema_std as well, as they use cov
3646+ # When the first data points are NaN, after the initial NaN is removed the next value that is removed has the wrong lookback weight applied to it
3647+
3648+ st = datetime (2020 , 1 , 1 )
3649+ N = 15
3650+ K = 3
3651+ horizon = 10
3652+ alpha = 0.1
3653+ values = [np .nan if i < K else float (i ) for i in range (N )]
3654+
3655+ @csp .graph
3656+ def g ():
3657+ x = csp .curve (
3658+ typ = float , data = [(st + timedelta (seconds = i ), values [i ]) for i in range (N )]
3659+ ) # start with some NaNs
3660+ ema_std = csp .stats .ema_std (x , alpha = alpha , adjust = True , bias = False , ignore_na = True , horizon = horizon )
3661+ csp .add_graph_output ("ema_std" , ema_std )
3662+
3663+ res = csp .run (g , starttime = st , endtime = timedelta (seconds = N ), output_numpy = True )
3664+
3665+ golden_ema_std = np .array (
3666+ [
3667+ pd .Series (values [max (0 , j - horizon + 1 ) : j + 1 ]).ewm (alpha = alpha , ignore_na = True ).std ().iloc [- 1 ]
3668+ for j in range (N )
3669+ ]
3670+ )
3671+ np .testing .assert_allclose (res ["ema_std" ][1 ], golden_ema_std , atol = 1e-10 )
3672+
36433673
36443674if __name__ == "__main__" :
36453675 unittest .main ()
0 commit comments