Skip to content

Commit bfca234

Browse files
committed
Add allow_non_overlapping parameter for bivariate statistics cov, corr and ema_cov
Signed-off-by: Adam Glustein <adamglustein@gmail.com>
1 parent 0e1dad5 commit bfca234

5 files changed

Lines changed: 153 additions & 61 deletions

File tree

cpp/csp/cppnodes/statsimpl.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,33 @@ DECLARE_CPPNODE( _in_sequence_check )
108108

109109
EXPORT_CPPNODE( _in_sequence_check );
110110

111+
/*
112+
@csp.node
113+
def _discard_non_overlapping(x: ts[float], y: ts[float]):
114+
*/
115+
116+
DECLARE_CPPNODE( _discard_non_overlapping )
117+
{
118+
TS_INPUT( double, x );
119+
TS_INPUT( double, y );
120+
121+
TS_NAMED_OUTPUT( double, x_sync );
122+
TS_NAMED_OUTPUT( double, y_sync );
123+
124+
INIT_CPPNODE( _discard_non_overlapping ) { }
125+
126+
INVOKE()
127+
{
128+
if( csp.ticked( x ) && csp.ticked( y ) )
129+
{
130+
x_sync.output( x );
131+
y_sync.output( y );
132+
}
133+
};
134+
};
135+
136+
EXPORT_CPPNODE( _discard_non_overlapping );
137+
111138
/*
112139
@csp.node(cppimpl=_cspstatsimpl._sync_nan_f)
113140
def _sync_nan_f(x: ts[float], y: ts[float]) -> csp.Outputs(x_sync=ts[float], y_sync=ts[float]):

cpp/csp/python/cspstatsimpl.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ REGISTER_CPPNODE( csp::cppnodes, _time_window_updates );
77
REGISTER_CPPNODE( csp::cppnodes, _cross_sectional_as_list );
88
REGISTER_CPPNODE( csp::cppnodes, _min_hit_by_tick );
99
REGISTER_CPPNODE( csp::cppnodes, _in_sequence_check );
10+
REGISTER_CPPNODE( csp::cppnodes, _discard_non_overlapping );
1011
REGISTER_CPPNODE( csp::cppnodes, _sync_nan_f );
1112

1213
// Base statistics

csp/stats.py

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ def _in_sequence_check(x: ts["T"], y: ts["T"]):
151151
raise NotImplementedError("_in_sequence_check only implemented in C++")
152152

153153

154+
@csp.node(cppimpl=_cspstatsimpl._discard_non_overlapping)
155+
def _discard_non_overlapping(x: ts[float], y: ts[float]) -> csp.Outputs(x_sync=ts[float], y_sync=ts[float]):
156+
raise NotImplementedError("_discard_non_overlapping only implemented in C++")
157+
return csp.output(x_sync=0, y_sync=0)
158+
159+
154160
@csp.node(cppimpl=_cspstatsimpl._sync_nan_f)
155161
def _sync_nan_f(x: ts[float], y: ts[float]) -> csp.Outputs(x_sync=ts[float], y_sync=ts[float]):
156162
raise NotImplementedError("_sync_nan_f only implemented in C++")
@@ -176,12 +182,6 @@ def _combine_signal(x: ts["T"], y: ts["U"]) -> ts[bool]:
176182
return True
177183

178184

179-
@csp.node
180-
def _combine_signal(x: ts["T"], y: ts["U"]) -> ts[bool]:
181-
if csp.ticked(x, y):
182-
return True
183-
184-
185185
@csp.node
186186
def _np_log(x: ts[np.ndarray]) -> ts[np.ndarray]:
187187
return np.log(x)
@@ -280,21 +280,32 @@ def _setup(x, interval, min_window, trigger, sampler, reset, weights=None, ignor
280280
return series, interval, min_window, trigger, min_hit, updates, sampler, reset, weights, recalc, clear_stat
281281

282282

283+
def _synchronize_bivariate(x, y, allow_non_overlapping):
284+
"""
285+
If allow_non_overlapping=True, discard any out-of-sync ticks between and y. Else, raise an exception when this occurs.
286+
"""
287+
in_seq = None
288+
if x is not y:
289+
if allow_non_overlapping:
290+
sync = _discard_non_overlapping(x, y)
291+
x, y = sync.x_sync, sync.y_sync
292+
else:
293+
in_seq = _in_sequence_check(x, y)
294+
return x, y, in_seq
295+
296+
283297
def _bivariate_setup(
284-
x, y, interval, min_window, trigger, sampler, reset, weights=None, ignore_weights=False, recalc=None
298+
x, y, interval, min_window, trigger, sampler, reset, weights=None, recalc=None, allow_non_overlapping=False
285299
):
286300
"""
287301
Sets up time-series window updates and triggers for a bivariate stats calculation
288302
"""
303+
x, y, in_seq = _synchronize_bivariate(x, y, allow_non_overlapping)
289304
x_upd = _setup(x, interval, min_window, trigger, sampler, reset, weights, False, recalc)[5]
290305
series, interval, min_window, trigger, min_hit, y_upd, sampler, reset, weights, recalc, clear_stat = _setup(
291306
y, interval, min_window, trigger, sampler, reset, weights, False, recalc
292307
)
293308

294-
in_seq = None
295-
if x is not y:
296-
in_seq = _in_sequence_check(x, y)
297-
298309
return (
299310
series,
300311
interval,
@@ -2181,25 +2192,27 @@ def cov(
21812192
reset: ts[object] = None,
21822193
recalc: ts[object] = None,
21832194
min_data_points: int = 0,
2195+
allow_non_overlapping: bool = False,
21842196
) -> ts[Union[float, np.ndarray]]:
21852197
"""
21862198
21872199
Returns the covariance between two in-sequence time-series within the given window. If the time-series are of type np.ndarray, the covariance is calculated elementwise.
21882200
21892201
Inputs
2190-
x: time series data, of type float or np.ndarray
2191-
y: time series data, of type float or np.ndarray, which ticks at the same time as x
2192-
interval: the window interval (either time or tick specified)
2193-
min_window: the minimum window (either time or tick specified) before statistics are returned. Must be the same type as interval
2194-
ddof: delta degrees of freedom
2195-
ignore_na: if True, will treat NaN values as missing data. If False, a NaN present in the window will make the computed statistic NaN as well
2196-
trigger: another time-series which specifies when you want to recalculate the statistic
2197-
weights: another time-series which specifies the weights to use on each x value, if a weighted covariance is desired
2198-
sampler: another time-series which specifies when x should tick. If x ticks when sampler does not, the data is ignored. If sampler ticks when x does not,
2199-
the data point is treated as NaN
2200-
reset: another time-series which will clear the data in the window when it ticks
2201-
recalc: another time-series which triggers a clean recalculation of the window statistic, and in doing so clears any accumulated floating-point error
2202-
min_data_points: minimum number of current ticks in the interval needed for a valid computation. If there are fewer ticks, NaN is returned.
2202+
x: time series data, of type float or np.ndarray
2203+
y: time series data, of type float or np.ndarray, which ticks at the same time as x
2204+
interval: the window interval (either time or tick specified)
2205+
min_window: the minimum window (either time or tick specified) before statistics are returned. Must be the same type as interval
2206+
ddof: delta degrees of freedom
2207+
ignore_na: if True, will treat NaN values as missing data. If False, a NaN present in the window will make the computed statistic NaN as well
2208+
trigger: another time-series which specifies when you want to recalculate the statistic
2209+
weights: another time-series which specifies the weights to use on each x value, if a weighted covariance is desired
2210+
sampler: another time-series which specifies when x should tick. If x ticks when sampler does not, the data is ignored. If sampler ticks when x does not,
2211+
the data point is treated as NaN
2212+
reset: another time-series which will clear the data in the window when it ticks
2213+
recalc: another time-series which triggers a clean recalculation of the window statistic, and in doing so clears any accumulated floating-point error
2214+
min_data_points: minimum number of current ticks in the interval needed for a valid computation. If there are fewer ticks, NaN is returned.
2215+
allow_non_overlapping: if True, discard any ticks of x and y that occur out-of-sync with one another. If False, raise an exception on any out-of-sync ticks.
22032216
22042217
"""
22052218

@@ -2217,7 +2230,7 @@ def cov(
22172230
recalc,
22182231
clear_stat,
22192232
in_seq,
2220-
) = _bivariate_setup(x, y, interval, min_window, trigger, sampler, reset, weights, True, recalc)
2233+
) = _bivariate_setup(x, y, interval, min_window, trigger, sampler, reset, weights, recalc, allow_non_overlapping)
22212234

22222235
# Use same "debiasing" for weighted/non-weighted
22232236
edge = None
@@ -2543,24 +2556,26 @@ def corr(
25432556
reset: ts[object] = None,
25442557
recalc: ts[object] = None,
25452558
min_data_points: int = 0,
2559+
allow_non_overlapping: bool = False,
25462560
) -> ts[Union[float, np.ndarray]]:
25472561
"""
25482562
25492563
Returns the correlation between x and y within the given window. If the time-series are of type np.ndarray, the correlation is calculated elementwise.
25502564
25512565
Inputs
2552-
x: time series data, of type float or np.ndarray
2553-
y: time series data, of type float or np.ndarray, which ticks at the same time x ticks
2554-
interval: the window interval (either time or tick specified)
2555-
min_window: the minimum window (either time or tick specified) before statistics are returned. Must be the same type as interval
2556-
ignore_na: if True, will treat NaN values as missing data. If False, a NaN present in the window will make the computed statistic NaN as well
2557-
trigger: another time-series which specifies when you want to recalculate the statistic
2558-
weights: another time-series which specifies the weights to use on each x value, if a weighted correlation is desired
2559-
sampler: another time-series which specifies when x should tick. If x ticks when sampler does not, the data is ignored. If sampler ticks when x does not,
2560-
the data point is treated as NaN
2561-
reset: another time-series which will clear the data in the window when it ticks
2562-
recalc: another time-series which triggers a clean recalculation of the window statistic, and in doing so clears any accumulated floating-point error
2563-
min_data_points: minimum number of current ticks in the interval needed for a valid computation. If there are fewer ticks, NaN is returned.
2566+
x: time series data, of type float or np.ndarray
2567+
y: time series data, of type float or np.ndarray, which ticks at the same time x ticks
2568+
interval: the window interval (either time or tick specified)
2569+
min_window: the minimum window (either time or tick specified) before statistics are returned. Must be the same type as interval
2570+
ignore_na: if True, will treat NaN values as missing data. If False, a NaN present in the window will make the computed statistic NaN as well
2571+
trigger: another time-series which specifies when you want to recalculate the statistic
2572+
weights: another time-series which specifies the weights to use on each x value, if a weighted correlation is desired
2573+
sampler: another time-series which specifies when x should tick. If x ticks when sampler does not, the data is ignored. If sampler ticks when x does not,
2574+
the data point is treated as NaN
2575+
reset: another time-series which will clear the data in the window when it ticks
2576+
recalc: another time-series which triggers a clean recalculation of the window statistic, and in doing so clears any accumulated floating-point error
2577+
min_data_points: minimum number of current ticks in the interval needed for a valid computation. If there are fewer ticks, NaN is returned.
2578+
allow_non_overlapping: if True, discard any ticks of x and y that occur out-of-sync with one another. If False, raise an exception on any out-of-sync ticks.
25642579
25652580
"""
25662581
(
@@ -2577,7 +2592,7 @@ def corr(
25772592
recalc,
25782593
clear_stat,
25792594
in_seq,
2580-
) = _bivariate_setup(x, y, interval, min_window, trigger, sampler, reset, weights, True, recalc)
2595+
) = _bivariate_setup(x, y, interval, min_window, trigger, sampler, reset, weights, recalc, allow_non_overlapping)
25812596

25822597
if series.tstype.typ is float:
25832598
if weights is not None:
@@ -2965,38 +2980,40 @@ def ema_cov(
29652980
reset: ts[object] = None,
29662981
recalc: ts[object] = None,
29672982
min_data_points: int = 0,
2983+
allow_non_overlapping: bool = False,
29682984
) -> ts[Union[float, np.ndarray]]:
29692985
"""
29702986
29712987
Returns the exponential moving covariance between two time series.
29722988
29732989
Inputs
2974-
x: time series data, of type float or np.ndarray
2975-
y: time series data, of type float or np.ndarray, which ticks at the same time as x
2976-
min_periods: the minimum number of data points before statistics are returned
2977-
alpha: specify the decay parameter in terms of alpha
2978-
span: specify the decay parameter in terms of span
2979-
com: specify the decay parameter in terms of com
2980-
halflife: specify the decay parameter in terms of halflife
2981-
adjust: if True, an adjusted EMA will be computed. If False, a standard (unadjusted) EMA will be computed
2982-
horizon: if specified, values that are older than the horizon will be removed entirely from the computation (essentially making EMA a window computation)
2983-
bias: if True, a biased EMA covariance is computed. If False, the covariance estimate is unbiased
2984-
ignore_na: if True, NaNs will be ignored and have no effect on the computation. If False, a NaN will shift the observation window once new non-NaN data comes in
2985-
trigger: another time-series which specifies when you want to recalculate the statistic
2986-
sampler: another time-series which specifies when x should tick. If x ticks when sampler does not, the data is ignored. If sampler ticks when x does not,
2987-
the data point is treated as NaN
2988-
reset: another time-series which will clear the data in the window when it ticks
2989-
recalc: only valid when a finite-horizon EMA is used. Another time-series which triggers a clean recalculation of the window statistic, and in doing so clears any accumulated floating-point error
2990-
min_data_points: minimum number of current ticks in the interval needed for a valid computation. If there are fewer ticks, NaN is returned.
2990+
x: time series data, of type float or np.ndarray
2991+
y: time series data, of type float or np.ndarray, which ticks at the same time as x
2992+
min_periods: the minimum number of data points before statistics are returned
2993+
alpha: specify the decay parameter in terms of alpha
2994+
span: specify the decay parameter in terms of span
2995+
com: specify the decay parameter in terms of com
2996+
halflife: specify the decay parameter in terms of halflife
2997+
adjust: if True, an adjusted EMA will be computed. If False, a standard (unadjusted) EMA will be computed
2998+
horizon: if specified, values that are older than the horizon will be removed entirely from the computation (essentially making EMA a window computation)
2999+
bias: if True, a biased EMA covariance is computed. If False, the covariance estimate is unbiased
3000+
ignore_na: if True, NaNs will be ignored and have no effect on the computation. If False, a NaN will shift the observation window once new non-NaN data comes in
3001+
trigger: another time-series which specifies when you want to recalculate the statistic
3002+
sampler: another time-series which specifies when x should tick. If x ticks when sampler does not, the data is ignored. If sampler ticks when x does not,
3003+
the data point is treated as NaN
3004+
reset: another time-series which will clear the data in the window when it ticks
3005+
recalc: only valid when a finite-horizon EMA is used. Another time-series which triggers a clean recalculation of the window statistic, and in doing so clears any accumulated floating-point error
3006+
min_data_points: minimum number of current ticks in the interval needed for a valid computation. If there are fewer ticks, NaN is returned.
3007+
allow_non_overlapping: if True, discard any ticks of x and y that occur out-of-sync with one another. If False, raise an exception on any out-of-sync ticks.
29913008
29923009
"""
29933010

29943011
alpha, interval, min_window, debias_horizon, recalc = _validate_ema(
29953012
alpha, span, com, halflife, adjust, horizon, recalc
29963013
)
29973014

3015+
x, y, _ = _synchronize_bivariate(x, y, allow_non_overlapping)
29983016
if x is not y:
2999-
_in_sequence_check(x, y)
30003017
sync = _sync_nan(x, y)
30013018
x, y = sync.x_sync, sync.y_sync
30023019

csp/tests/test_stats.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3599,6 +3599,47 @@ def g():
35993599
self.assertTrue(pd.Series(res["kurt"][1]).isna().all())
36003600
self.assertTrue(pd.Series(res["corr"][1]).isna().all())
36013601

3602+
def test_allow_non_overlapping_bivariate(self):
3603+
st = datetime(2020, 1, 1)
3604+
3605+
@csp.graph
3606+
def g(allow_non_overlapping: bool):
3607+
x = csp.curve(
3608+
typ=float,
3609+
data=[
3610+
(datetime(2020, 1, 1), 1),
3611+
(datetime(2020, 1, 2), 2),
3612+
(datetime(2020, 1, 4), 4), # discarded
3613+
(datetime(2020, 1, 6), 6), # discarded
3614+
(datetime(2020, 1, 7), 7),
3615+
],
3616+
)
3617+
y = csp.curve(
3618+
typ=float,
3619+
data=[
3620+
(datetime(2020, 1, 1), 1),
3621+
(datetime(2020, 1, 2), 2),
3622+
(datetime(2020, 1, 3), -1), # discarded
3623+
(datetime(2020, 1, 5), -2), # discarded
3624+
(datetime(2020, 1, 7), 7),
3625+
],
3626+
)
3627+
cov = csp.stats.cov(x, y, 5, 1, allow_non_overlapping=allow_non_overlapping)
3628+
corr = csp.stats.corr(x, y, 5, 1, allow_non_overlapping=allow_non_overlapping)
3629+
ema_cov = csp.stats.ema_cov(x, y, 1, alpha=0.1, allow_non_overlapping=allow_non_overlapping)
3630+
csp.add_graph_output("cov", cov)
3631+
csp.add_graph_output("corr", corr)
3632+
csp.add_graph_output("ema_cov", ema_cov)
3633+
3634+
res = csp.run(g, True, starttime=st, endtime=timedelta(days=8), output_numpy=True)
3635+
for output in res.values():
3636+
# Convert expected datetimes to the same type as output
3637+
expected_dates = pd.to_datetime([datetime(2020, 1, i) for i in (1, 2, 7)])
3638+
np.testing.assert_array_equal(output[0], expected_dates.values)
3639+
3640+
# Additional sanity check is that correlation should be 1 as middle ticks are ignored
3641+
np.testing.assert_allclose(res["corr"][1], np.array([np.nan, 1.0, 1.0]), equal_nan=True)
3642+
36023643

36033644
if __name__ == "__main__":
36043645
unittest.main()

0 commit comments

Comments
 (0)