factorflow 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- factorflow/__init__.py +59 -0
- factorflow/bars.py +246 -0
- factorflow/batch.py +221 -0
- factorflow/events.py +610 -0
- factorflow/graph.py +177 -0
- factorflow/grid.py +298 -0
- factorflow/kernels.py +434 -0
- factorflow/live.py +554 -0
- factorflow/panel.py +181 -0
- factorflow/py.typed +0 -0
- factorflow/sim/__init__.py +22 -0
- factorflow/sim/__main__.py +210 -0
- factorflow/sim/bench.py +130 -0
- factorflow/sim/market.py +669 -0
- factorflow/sim/pipelines.py +147 -0
- factorflow/sim/replay.py +385 -0
- factorflow/sim/validate.py +464 -0
- factorflow/terms.py +1921 -0
- factorflow-0.1.0.dist-info/METADATA +368 -0
- factorflow-0.1.0.dist-info/RECORD +23 -0
- factorflow-0.1.0.dist-info/WHEEL +4 -0
- factorflow-0.1.0.dist-info/licenses/LICENSE +202 -0
- factorflow-0.1.0.dist-info/licenses/NOTICE +14 -0
factorflow/terms.py
ADDED
|
@@ -0,0 +1,1921 @@
|
|
|
1
|
+
"""Terms: the immutable, hashable nodes of a factor graph.
|
|
2
|
+
|
|
3
|
+
A term declares its ``inputs``, a ``window`` (how many rows of each input one output row
|
|
4
|
+
needs) and how to compute. Three kinds, as in Zipline's Pipeline:
|
|
5
|
+
|
|
6
|
+
* :class:`Factor`, float64 (NaN = missing)
|
|
7
|
+
* :class:`Filter`, bool (False = excluded)
|
|
8
|
+
* :class:`Classifier`, int64 group codes (-1 = missing)
|
|
9
|
+
|
|
10
|
+
Every term implements ``compute(blocks, prev)``: ``blocks[i]`` holds ``T_in`` rows of input
|
|
11
|
+
``i`` and the result has ``T_in - window + 1`` rows. Batch calls it on the whole history,
|
|
12
|
+
live calls it on exactly ``window`` rows. Row-wise terms (cross-sectional, recursive) are
|
|
13
|
+
looped one row at a time in both executors, so both run identical code on identical data.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import operator
|
|
19
|
+
from collections.abc import Callable, Sequence
|
|
20
|
+
from typing import Any, ClassVar
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from factorflow import kernels as K
|
|
25
|
+
|
|
26
|
+
Scalar = int | float | bool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Term:
|
|
30
|
+
"""A node in a factor graph.
|
|
31
|
+
|
|
32
|
+
Terms are immutable. Two terms are the same computation when their class,
|
|
33
|
+
``params``, ``window``, and input identities match; the compiler uses that
|
|
34
|
+
as the common-subexpression key. ``==`` is identity, not structural
|
|
35
|
+
equality, so ``close == close`` is not a filter. Value comparisons are
|
|
36
|
+
methods (:meth:`Factor.eq`).
|
|
37
|
+
|
|
38
|
+
Subclasses set the class attributes and implement :meth:`compute` or
|
|
39
|
+
:meth:`compute_row`.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
kind : str
|
|
44
|
+
``"factor"``, ``"filter"``, ``"classifier"``, or ``"term"``.
|
|
45
|
+
dtype : type
|
|
46
|
+
Numpy dtype of the output. float64, bool, or int64.
|
|
47
|
+
missing
|
|
48
|
+
Fill used where a value does not exist yet. NaN, ``False``, or ``-1``.
|
|
49
|
+
window : int
|
|
50
|
+
How many input rows one output row needs, including the current row.
|
|
51
|
+
A 5-bar rolling mean has window 5. A 1-bar lag has window 2, because
|
|
52
|
+
the kernel sees the lagged row and the current row.
|
|
53
|
+
columnwise : bool
|
|
54
|
+
``True`` when output column ``j`` depends only on input column ``j``.
|
|
55
|
+
The live engine may then recompute dirty columns alone.
|
|
56
|
+
rowwise : bool
|
|
57
|
+
``True`` when both executors loop one row at a time (cross-section
|
|
58
|
+
and EWMA). Those terms implement :meth:`compute_row`.
|
|
59
|
+
recursive : bool
|
|
60
|
+
``True`` when the term needs its own previous output (EWMA).
|
|
61
|
+
inputs : tuple of Term
|
|
62
|
+
Child nodes. Empty for leaves.
|
|
63
|
+
params : tuple
|
|
64
|
+
Extra identity: window kind, half-life, comparison symbol, constants.
|
|
65
|
+
Must be hashable.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
kind: ClassVar[str] = "term"
|
|
69
|
+
dtype: ClassVar[type] = np.float64
|
|
70
|
+
missing: ClassVar[Any] = np.nan
|
|
71
|
+
window: int = 1
|
|
72
|
+
#: output column j depends only on input column j (so dirty columns can be recomputed alone)
|
|
73
|
+
columnwise: bool = True
|
|
74
|
+
#: computed one row at a time (cross-sectional or recursive) in both executors
|
|
75
|
+
rowwise: bool = False
|
|
76
|
+
#: needs its own previous sealed output (EWMA)
|
|
77
|
+
recursive: bool = False
|
|
78
|
+
|
|
79
|
+
inputs: tuple[Term, ...] = ()
|
|
80
|
+
params: tuple = ()
|
|
81
|
+
|
|
82
|
+
# -- identity -------------------------------------------------------------------
|
|
83
|
+
def _key(self) -> tuple:
|
|
84
|
+
return (type(self).__name__, self.params, self.window, tuple(id(i) for i in self.inputs))
|
|
85
|
+
|
|
86
|
+
def __hash__(self) -> int:
|
|
87
|
+
"""Hash the structural key (class, params, window, input identities)."""
|
|
88
|
+
return hash(self._key())
|
|
89
|
+
|
|
90
|
+
def __eq__(self, other: object) -> bool: # identity semantics; use .eq() for comparisons
|
|
91
|
+
"""Return whether ``other`` is this same node.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
other : object
|
|
96
|
+
Compared with ``is``, not by value.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
bool
|
|
101
|
+
``True`` only when ``other`` is this object. Use :meth:`Factor.eq`
|
|
102
|
+
to compare numeric values.
|
|
103
|
+
"""
|
|
104
|
+
return self is other
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
"""Return ``ClassName(inputs, params)`` for debugging."""
|
|
108
|
+
args = ", ".join([repr(i) for i in self.inputs] + [repr(p) for p in self.params])
|
|
109
|
+
return f"{type(self).__name__}({args})"
|
|
110
|
+
|
|
111
|
+
def compute(self, blocks: Sequence[np.ndarray], prev: np.ndarray | None = None) -> np.ndarray:
|
|
112
|
+
"""Evaluate the term on a block of rows.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
blocks : sequence of ndarray
|
|
117
|
+
``blocks[i]`` has shape ``(T_in, N)`` and is input ``i``. For a
|
|
118
|
+
windowed term, ``T_in`` is the padded history in batch and exactly
|
|
119
|
+
``window`` in live.
|
|
120
|
+
prev : ndarray of shape (N,) or None, optional
|
|
121
|
+
Previous output of a recursive term. Ignored by everyone else.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
ndarray
|
|
126
|
+
Shape ``(T_in - window + 1, N)`` for windowed terms, or ``(T_in, N)``
|
|
127
|
+
when ``window`` is 1. Live keeps row 0 of that array, which is the
|
|
128
|
+
only row when ``T_in == window``.
|
|
129
|
+
|
|
130
|
+
Raises
|
|
131
|
+
------
|
|
132
|
+
NotImplementedError
|
|
133
|
+
The base class does not compute.
|
|
134
|
+
"""
|
|
135
|
+
raise NotImplementedError
|
|
136
|
+
|
|
137
|
+
def compute_row(self, rows: Sequence[np.ndarray], prev: np.ndarray | None) -> np.ndarray:
|
|
138
|
+
"""Evaluate one row of a row-wise term.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
rows : sequence of ndarray
|
|
143
|
+
``rows[i]`` has shape ``(1, N)`` or ``(window, N)``. Row-wise
|
|
144
|
+
terms in this package read the last row.
|
|
145
|
+
prev : ndarray of shape (N,) or None
|
|
146
|
+
Previous sealed output for a recursive term, or ``None`` on the
|
|
147
|
+
first row.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
ndarray of shape (N,)
|
|
152
|
+
The output row.
|
|
153
|
+
|
|
154
|
+
Raises
|
|
155
|
+
------
|
|
156
|
+
NotImplementedError
|
|
157
|
+
The base class does not compute.
|
|
158
|
+
"""
|
|
159
|
+
raise NotImplementedError
|
|
160
|
+
|
|
161
|
+
def nonnull(self) -> np.ndarray: # pragma: no cover - helper for subclasses
|
|
162
|
+
"""Return a mask of values that are present.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
ndarray of bool
|
|
167
|
+
|
|
168
|
+
Raises
|
|
169
|
+
------
|
|
170
|
+
NotImplementedError
|
|
171
|
+
Subclasses that need this override it. The engines do not call it.
|
|
172
|
+
"""
|
|
173
|
+
raise NotImplementedError
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _lift(x: Term | Scalar) -> Term:
|
|
177
|
+
if isinstance(x, Term):
|
|
178
|
+
return x
|
|
179
|
+
return Constant(x)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ====================================================================== kinds
|
|
183
|
+
class Factor(Term):
|
|
184
|
+
"""A floating-point expression. Missing values are NaN.
|
|
185
|
+
|
|
186
|
+
Notes
|
|
187
|
+
-----
|
|
188
|
+
Arithmetic and comparisons build new terms; they do not look at data.
|
|
189
|
+
``close + 1`` is a term you can put in a :class:`~factorflow.graph.Pipeline`.
|
|
190
|
+
Comparisons return a :class:`Filter`. ``==`` on two terms is identity
|
|
191
|
+
(``is``); use :meth:`eq` to compare values.
|
|
192
|
+
|
|
193
|
+
Examples
|
|
194
|
+
--------
|
|
195
|
+
>>> import factorflow as ff
|
|
196
|
+
>>> close = ff.Column('close')
|
|
197
|
+
>>> (close.pct_change(1) > 0).kind
|
|
198
|
+
'filter'
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
kind = "factor"
|
|
202
|
+
dtype = np.float64
|
|
203
|
+
missing = np.nan
|
|
204
|
+
|
|
205
|
+
# arithmetic
|
|
206
|
+
def __add__(self, o):
|
|
207
|
+
"""Add a factor or a scalar, elementwise.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
o : Factor or int or float or bool
|
|
212
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
Factor
|
|
217
|
+
``self + o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
218
|
+
"""
|
|
219
|
+
return BinOp(operator.add, "+", self, _lift(o))
|
|
220
|
+
|
|
221
|
+
def __radd__(self, o):
|
|
222
|
+
"""Add a scalar on the left, elementwise.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
o : Factor or int or float or bool
|
|
227
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
Factor
|
|
232
|
+
``self + o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
233
|
+
"""
|
|
234
|
+
return BinOp(operator.add, "+", _lift(o), self)
|
|
235
|
+
|
|
236
|
+
def __sub__(self, o):
|
|
237
|
+
"""Subtract a factor or a scalar, elementwise.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
o : Factor or int or float or bool
|
|
242
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
Factor
|
|
247
|
+
``self - o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
248
|
+
"""
|
|
249
|
+
return BinOp(operator.sub, "-", self, _lift(o))
|
|
250
|
+
|
|
251
|
+
def __rsub__(self, o):
|
|
252
|
+
"""Subtract this factor from a scalar, elementwise.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
o : Factor or int or float or bool
|
|
257
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
Factor
|
|
262
|
+
``self - o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
263
|
+
"""
|
|
264
|
+
return BinOp(operator.sub, "-", _lift(o), self)
|
|
265
|
+
|
|
266
|
+
def __mul__(self, o):
|
|
267
|
+
"""Multiply by a factor or a scalar, elementwise.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
o : Factor or int or float or bool
|
|
272
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
Factor
|
|
277
|
+
``self * o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
278
|
+
"""
|
|
279
|
+
return BinOp(operator.mul, "*", self, _lift(o))
|
|
280
|
+
|
|
281
|
+
def __rmul__(self, o):
|
|
282
|
+
"""Multiply a scalar on the left, elementwise.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
o : Factor or int or float or bool
|
|
287
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
Factor
|
|
292
|
+
``self * o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
293
|
+
"""
|
|
294
|
+
return BinOp(operator.mul, "*", _lift(o), self)
|
|
295
|
+
|
|
296
|
+
def __truediv__(self, o):
|
|
297
|
+
"""Divide by a factor or a scalar, elementwise.
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
o : Factor or int or float or bool
|
|
302
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
Factor
|
|
307
|
+
``self / o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
308
|
+
"""
|
|
309
|
+
return BinOp(operator.truediv, "/", self, _lift(o))
|
|
310
|
+
|
|
311
|
+
def __rtruediv__(self, o):
|
|
312
|
+
"""Divide a scalar by this factor, elementwise.
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
o : Factor or int or float or bool
|
|
317
|
+
The other operand. A number is broadcast to every asset and bar.
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
Factor
|
|
322
|
+
``self / o`` when this factor is on the left, otherwise the reflected form. NaN propagates.
|
|
323
|
+
"""
|
|
324
|
+
return BinOp(operator.truediv, "/", _lift(o), self)
|
|
325
|
+
|
|
326
|
+
def __neg__(self):
|
|
327
|
+
"""Negate every value.
|
|
328
|
+
|
|
329
|
+
Returns
|
|
330
|
+
-------
|
|
331
|
+
Factor
|
|
332
|
+
``-self``. NaN stays NaN.
|
|
333
|
+
"""
|
|
334
|
+
return Unary(np.negative, "neg", self)
|
|
335
|
+
|
|
336
|
+
def __abs__(self):
|
|
337
|
+
"""Take the absolute value.
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
Factor
|
|
342
|
+
``abs(self)``.
|
|
343
|
+
"""
|
|
344
|
+
return Unary(np.abs, "abs", self)
|
|
345
|
+
|
|
346
|
+
# comparisons -> Filter
|
|
347
|
+
def __lt__(self, o):
|
|
348
|
+
"""Test strictly less than.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
o : Factor or int or float or bool
|
|
353
|
+
The value to compare against. A number is broadcast.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
Filter
|
|
358
|
+
``self < o``. A comparison with NaN is False.
|
|
359
|
+
"""
|
|
360
|
+
return Compare(operator.lt, "<", self, _lift(o))
|
|
361
|
+
|
|
362
|
+
def __le__(self, o):
|
|
363
|
+
"""Test less than or equal.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
o : Factor or int or float or bool
|
|
368
|
+
The value to compare against. A number is broadcast.
|
|
369
|
+
|
|
370
|
+
Returns
|
|
371
|
+
-------
|
|
372
|
+
Filter
|
|
373
|
+
``self <= o``. A comparison with NaN is False.
|
|
374
|
+
"""
|
|
375
|
+
return Compare(operator.le, "<=", self, _lift(o))
|
|
376
|
+
|
|
377
|
+
def __gt__(self, o):
|
|
378
|
+
"""Test strictly greater than.
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
o : Factor or int or float or bool
|
|
383
|
+
The value to compare against. A number is broadcast.
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
Filter
|
|
388
|
+
``self > o``. A comparison with NaN is False.
|
|
389
|
+
"""
|
|
390
|
+
return Compare(operator.gt, ">", self, _lift(o))
|
|
391
|
+
|
|
392
|
+
def __ge__(self, o):
|
|
393
|
+
"""Test greater than or equal.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
o : Factor or int or float or bool
|
|
398
|
+
The value to compare against. A number is broadcast.
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
Filter
|
|
403
|
+
``self >= o``. A comparison with NaN is False.
|
|
404
|
+
"""
|
|
405
|
+
return Compare(operator.ge, ">=", self, _lift(o))
|
|
406
|
+
|
|
407
|
+
def eq(self, o):
|
|
408
|
+
"""Test equality of values.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
o : Factor or int or float or bool
|
|
413
|
+
The value to compare against. A number is broadcast.
|
|
414
|
+
|
|
415
|
+
Returns
|
|
416
|
+
-------
|
|
417
|
+
Filter
|
|
418
|
+
``self == o`` as a filter. This is not Python ``==``, which tests node identity.
|
|
419
|
+
"""
|
|
420
|
+
return Compare(operator.eq, "==", self, _lift(o))
|
|
421
|
+
|
|
422
|
+
def ne(self, o):
|
|
423
|
+
"""Test inequality of values.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
o : Factor or int or float or bool
|
|
428
|
+
The value to compare against. A number is broadcast.
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
Filter
|
|
433
|
+
``self != o`` as a filter. A comparison with NaN is False.
|
|
434
|
+
"""
|
|
435
|
+
return Compare(operator.ne, "!=", self, _lift(o))
|
|
436
|
+
|
|
437
|
+
__hash__ = Term.__hash__
|
|
438
|
+
|
|
439
|
+
def log(self) -> Factor:
|
|
440
|
+
"""Take the natural logarithm.
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
Factor
|
|
445
|
+
``log(self)``. Non-positive inputs are NaN. Computed on full rows so live and batch match.
|
|
446
|
+
"""
|
|
447
|
+
return Unary(np.log, "log", self, columnwise=False)
|
|
448
|
+
|
|
449
|
+
def sqrt(self) -> Factor:
|
|
450
|
+
"""Take the square root.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
Factor
|
|
455
|
+
``sqrt(self)``. Negative inputs are NaN.
|
|
456
|
+
"""
|
|
457
|
+
return Unary(np.sqrt, "sqrt", self)
|
|
458
|
+
|
|
459
|
+
def isnull(self) -> Filter:
|
|
460
|
+
"""Test for missing values.
|
|
461
|
+
|
|
462
|
+
Returns
|
|
463
|
+
-------
|
|
464
|
+
Filter
|
|
465
|
+
True where this factor is NaN.
|
|
466
|
+
"""
|
|
467
|
+
return IsNull(self)
|
|
468
|
+
|
|
469
|
+
def notnull(self) -> Filter:
|
|
470
|
+
"""Test for present values.
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
Filter
|
|
475
|
+
True where this factor is not NaN.
|
|
476
|
+
"""
|
|
477
|
+
return ~IsNull(self)
|
|
478
|
+
|
|
479
|
+
def fillna(self, value: float) -> Factor:
|
|
480
|
+
"""Replace missing values.
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
value : float
|
|
485
|
+
Replacement, in the same units as the factor. Applied where the value is NaN.
|
|
486
|
+
|
|
487
|
+
Returns
|
|
488
|
+
-------
|
|
489
|
+
Factor
|
|
490
|
+
``self`` with NaNs replaced by ``value``.
|
|
491
|
+
"""
|
|
492
|
+
return FillNa(self, float(value))
|
|
493
|
+
|
|
494
|
+
# time series
|
|
495
|
+
def shift(self, n: int = 1) -> Factor:
|
|
496
|
+
"""Lag by ``n`` bars.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
n : int, optional
|
|
501
|
+
Bars to look back. Default is 1. The first ``n`` rows are NaN.
|
|
502
|
+
|
|
503
|
+
Returns
|
|
504
|
+
-------
|
|
505
|
+
Factor
|
|
506
|
+
``self`` from ``n`` bars earlier.
|
|
507
|
+
|
|
508
|
+
Raises
|
|
509
|
+
------
|
|
510
|
+
ValueError
|
|
511
|
+
If ``n < 1``.
|
|
512
|
+
"""
|
|
513
|
+
return Shift(self, n)
|
|
514
|
+
|
|
515
|
+
def delta(self, n: int = 1) -> Factor:
|
|
516
|
+
"""Subtract the value from ``n`` bars ago.
|
|
517
|
+
|
|
518
|
+
Parameters
|
|
519
|
+
----------
|
|
520
|
+
n : int, optional
|
|
521
|
+
Bars to look back. Default is 1.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
Factor
|
|
526
|
+
``self - self.shift(n)``.
|
|
527
|
+
|
|
528
|
+
Raises
|
|
529
|
+
------
|
|
530
|
+
ValueError
|
|
531
|
+
If ``n < 1``.
|
|
532
|
+
"""
|
|
533
|
+
return self - self.shift(n)
|
|
534
|
+
|
|
535
|
+
def pct_change(self, n: int = 1) -> Factor:
|
|
536
|
+
"""Return the fractional change over ``n`` bars.
|
|
537
|
+
|
|
538
|
+
Parameters
|
|
539
|
+
----------
|
|
540
|
+
n : int, optional
|
|
541
|
+
Bars to look back. Default is 1.
|
|
542
|
+
|
|
543
|
+
Returns
|
|
544
|
+
-------
|
|
545
|
+
Factor
|
|
546
|
+
``self / self.shift(n) - 1``. Dimensionless. NaN when the lagged value is 0 or missing.
|
|
547
|
+
|
|
548
|
+
Raises
|
|
549
|
+
------
|
|
550
|
+
ValueError
|
|
551
|
+
If ``n < 1``.
|
|
552
|
+
"""
|
|
553
|
+
return self / self.shift(n) - 1.0
|
|
554
|
+
|
|
555
|
+
def rolling_sum(self, w: int) -> Factor:
|
|
556
|
+
"""Return the rolling sum.
|
|
557
|
+
|
|
558
|
+
Parameters
|
|
559
|
+
----------
|
|
560
|
+
w : int
|
|
561
|
+
Window length in bars (rows), not calendar time. A window that contains a NaN is NaN.
|
|
562
|
+
|
|
563
|
+
Returns
|
|
564
|
+
-------
|
|
565
|
+
Factor
|
|
566
|
+
the rolling sum.
|
|
567
|
+
|
|
568
|
+
Raises
|
|
569
|
+
------
|
|
570
|
+
ValueError
|
|
571
|
+
If ``w < 1``.
|
|
572
|
+
"""
|
|
573
|
+
return Rolling(self, w, "sum")
|
|
574
|
+
|
|
575
|
+
def rolling_mean(self, w: int) -> Factor:
|
|
576
|
+
"""Return the rolling mean.
|
|
577
|
+
|
|
578
|
+
Parameters
|
|
579
|
+
----------
|
|
580
|
+
w : int
|
|
581
|
+
Window length in bars (rows), not calendar time. A window that contains a NaN is NaN.
|
|
582
|
+
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
Factor
|
|
586
|
+
the rolling mean.
|
|
587
|
+
|
|
588
|
+
Raises
|
|
589
|
+
------
|
|
590
|
+
ValueError
|
|
591
|
+
If ``w < 1``.
|
|
592
|
+
"""
|
|
593
|
+
return Rolling(self, w, "mean")
|
|
594
|
+
|
|
595
|
+
def rolling_var(self, w: int) -> Factor:
|
|
596
|
+
"""Return the rolling sample variance.
|
|
597
|
+
|
|
598
|
+
Parameters
|
|
599
|
+
----------
|
|
600
|
+
w : int
|
|
601
|
+
Window length in bars. Must be at least 2. Divisor is ``w - 1``.
|
|
602
|
+
|
|
603
|
+
Returns
|
|
604
|
+
-------
|
|
605
|
+
Factor
|
|
606
|
+
NaN when the window contains a NaN.
|
|
607
|
+
|
|
608
|
+
Raises
|
|
609
|
+
------
|
|
610
|
+
ValueError
|
|
611
|
+
If ``w < 2``.
|
|
612
|
+
"""
|
|
613
|
+
return Rolling(self, w, "var")
|
|
614
|
+
|
|
615
|
+
def rolling_std(self, w: int) -> Factor:
|
|
616
|
+
"""Return the rolling sample standard deviation.
|
|
617
|
+
|
|
618
|
+
Parameters
|
|
619
|
+
----------
|
|
620
|
+
w : int
|
|
621
|
+
Window length in bars. Must be at least 2. Divisor is ``w - 1``.
|
|
622
|
+
|
|
623
|
+
Returns
|
|
624
|
+
-------
|
|
625
|
+
Factor
|
|
626
|
+
NaN when the window contains a NaN.
|
|
627
|
+
|
|
628
|
+
Raises
|
|
629
|
+
------
|
|
630
|
+
ValueError
|
|
631
|
+
If ``w < 2``.
|
|
632
|
+
"""
|
|
633
|
+
return Rolling(self, w, "std")
|
|
634
|
+
|
|
635
|
+
def rolling_min(self, w: int) -> Factor:
|
|
636
|
+
"""Return the rolling minimum.
|
|
637
|
+
|
|
638
|
+
Parameters
|
|
639
|
+
----------
|
|
640
|
+
w : int
|
|
641
|
+
Window length in bars (rows), not calendar time. A window that contains a NaN is NaN.
|
|
642
|
+
|
|
643
|
+
Returns
|
|
644
|
+
-------
|
|
645
|
+
Factor
|
|
646
|
+
the rolling minimum.
|
|
647
|
+
|
|
648
|
+
Raises
|
|
649
|
+
------
|
|
650
|
+
ValueError
|
|
651
|
+
If ``w < 1``.
|
|
652
|
+
"""
|
|
653
|
+
return Rolling(self, w, "min")
|
|
654
|
+
|
|
655
|
+
def rolling_max(self, w: int) -> Factor:
|
|
656
|
+
"""Return the rolling maximum.
|
|
657
|
+
|
|
658
|
+
Parameters
|
|
659
|
+
----------
|
|
660
|
+
w : int
|
|
661
|
+
Window length in bars (rows), not calendar time. A window that contains a NaN is NaN.
|
|
662
|
+
|
|
663
|
+
Returns
|
|
664
|
+
-------
|
|
665
|
+
Factor
|
|
666
|
+
the rolling maximum.
|
|
667
|
+
|
|
668
|
+
Raises
|
|
669
|
+
------
|
|
670
|
+
ValueError
|
|
671
|
+
If ``w < 1``.
|
|
672
|
+
"""
|
|
673
|
+
return Rolling(self, w, "max")
|
|
674
|
+
|
|
675
|
+
def ts_rank(self, w: int) -> Factor:
|
|
676
|
+
"""Return the time-series rank of the current value inside the window.
|
|
677
|
+
|
|
678
|
+
Parameters
|
|
679
|
+
----------
|
|
680
|
+
w : int
|
|
681
|
+
Window length in bars (rows), not calendar time. A window that contains a NaN is NaN.
|
|
682
|
+
|
|
683
|
+
Returns
|
|
684
|
+
-------
|
|
685
|
+
Factor
|
|
686
|
+
Fraction of the window less than or equal to the newest value, in ``(1/w, ..., 1)``.
|
|
687
|
+
|
|
688
|
+
Raises
|
|
689
|
+
------
|
|
690
|
+
ValueError
|
|
691
|
+
If ``w < 1``.
|
|
692
|
+
"""
|
|
693
|
+
return Rolling(self, w, "tsrank")
|
|
694
|
+
|
|
695
|
+
def rolling_cov(self, other: Factor, w: int) -> Factor:
|
|
696
|
+
"""Return the rolling sample covariance with ``other``.
|
|
697
|
+
|
|
698
|
+
Parameters
|
|
699
|
+
----------
|
|
700
|
+
other : Factor
|
|
701
|
+
The other series, paired asset by asset.
|
|
702
|
+
w : int
|
|
703
|
+
Window length in bars. Must be at least 2.
|
|
704
|
+
|
|
705
|
+
Returns
|
|
706
|
+
-------
|
|
707
|
+
Factor
|
|
708
|
+
Covariance. NaN if either window contains a NaN.
|
|
709
|
+
|
|
710
|
+
Raises
|
|
711
|
+
------
|
|
712
|
+
ValueError
|
|
713
|
+
If ``w < 2``.
|
|
714
|
+
"""
|
|
715
|
+
return Rolling2(self, other, w, "cov")
|
|
716
|
+
|
|
717
|
+
def rolling_corr(self, other: Factor, w: int) -> Factor:
|
|
718
|
+
"""Return the rolling Pearson correlation with ``other``.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
other : Factor
|
|
723
|
+
The other series, paired asset by asset.
|
|
724
|
+
w : int
|
|
725
|
+
Window length in bars. Must be at least 2.
|
|
726
|
+
|
|
727
|
+
Returns
|
|
728
|
+
-------
|
|
729
|
+
Factor
|
|
730
|
+
Correlation in ``[-1, 1]``. NaN if a window is constant or contains a NaN.
|
|
731
|
+
|
|
732
|
+
Raises
|
|
733
|
+
------
|
|
734
|
+
ValueError
|
|
735
|
+
If ``w < 2``.
|
|
736
|
+
"""
|
|
737
|
+
return Rolling2(self, other, w, "corr")
|
|
738
|
+
|
|
739
|
+
def rolling_beta(self, market: Factor, w: int) -> Factor:
|
|
740
|
+
"""Return the rolling regression slope on ``market``.
|
|
741
|
+
|
|
742
|
+
Parameters
|
|
743
|
+
----------
|
|
744
|
+
market : Factor
|
|
745
|
+
The independent series. Usually a market return broadcast to every asset.
|
|
746
|
+
w : int
|
|
747
|
+
Window length in bars. Must be at least 2.
|
|
748
|
+
|
|
749
|
+
Returns
|
|
750
|
+
-------
|
|
751
|
+
Factor
|
|
752
|
+
``cov(self, market) / var(market)``. NaN when the market window has no variance.
|
|
753
|
+
|
|
754
|
+
Raises
|
|
755
|
+
------
|
|
756
|
+
ValueError
|
|
757
|
+
If ``w < 2``.
|
|
758
|
+
"""
|
|
759
|
+
return Rolling2(self, market, w, "cov") / Rolling(market, w, "var")
|
|
760
|
+
|
|
761
|
+
def ewm_mean(self, halflife: float) -> Factor:
|
|
762
|
+
"""Return the exponentially weighted mean.
|
|
763
|
+
|
|
764
|
+
Parameters
|
|
765
|
+
----------
|
|
766
|
+
halflife : float
|
|
767
|
+
Half-life in bars. Must be positive. A half-life of 1 halves the previous mean each bar.
|
|
768
|
+
|
|
769
|
+
Returns
|
|
770
|
+
-------
|
|
771
|
+
Factor
|
|
772
|
+
Recursive mean. A NaN observation keeps the previous mean instead of wiping it.
|
|
773
|
+
|
|
774
|
+
Raises
|
|
775
|
+
------
|
|
776
|
+
ValueError
|
|
777
|
+
If ``halflife <= 0``.
|
|
778
|
+
"""
|
|
779
|
+
return EWMMean(self, halflife)
|
|
780
|
+
|
|
781
|
+
# cross-sectional
|
|
782
|
+
def rank(
|
|
783
|
+
self, *, mask: Filter | None = None, groupby: Classifier | None = None, ascending: bool = True
|
|
784
|
+
) -> Factor:
|
|
785
|
+
"""Rank across assets on this bar.
|
|
786
|
+
|
|
787
|
+
Parameters
|
|
788
|
+
----------
|
|
789
|
+
mask : Filter or None, optional
|
|
790
|
+
Who is eligible. ``None`` uses every finite value. Default is ``None``.
|
|
791
|
+
groupby : Classifier or None, optional
|
|
792
|
+
Group codes. ``None`` is one group. ``-1`` is missing and is dropped. Default is ``None``.
|
|
793
|
+
ascending : bool, optional
|
|
794
|
+
``True`` gives rank 1 to the smallest value. Default is ``True``.
|
|
795
|
+
|
|
796
|
+
Returns
|
|
797
|
+
-------
|
|
798
|
+
Factor
|
|
799
|
+
Ranks ``1 .. k`` inside each group. Ties break by column order.
|
|
800
|
+
"""
|
|
801
|
+
return CrossSection(self, "rank", mask, groupby, ascending)
|
|
802
|
+
|
|
803
|
+
def zscore(self, *, mask: Filter | None = None, groupby: Classifier | None = None) -> Factor:
|
|
804
|
+
"""Standardize across assets on this bar.
|
|
805
|
+
|
|
806
|
+
Parameters
|
|
807
|
+
----------
|
|
808
|
+
mask : Filter or None, optional
|
|
809
|
+
Who is eligible. ``None`` uses every finite value. Default is ``None``.
|
|
810
|
+
groupby : Classifier or None, optional
|
|
811
|
+
Group codes. ``None`` is one group. ``-1`` is missing and is dropped. Default is ``None``.
|
|
812
|
+
|
|
813
|
+
Returns
|
|
814
|
+
-------
|
|
815
|
+
Factor
|
|
816
|
+
``(x - mean) / sample_std`` inside the group. NaN if the group
|
|
817
|
+
has fewer than two values or zero dispersion.
|
|
818
|
+
"""
|
|
819
|
+
return CrossSection(self, "zscore", mask, groupby)
|
|
820
|
+
|
|
821
|
+
def demean(self, *, mask: Filter | None = None, groupby: Classifier | None = None) -> Factor:
|
|
822
|
+
"""Subtract the cross-sectional mean on this bar.
|
|
823
|
+
|
|
824
|
+
Parameters
|
|
825
|
+
----------
|
|
826
|
+
mask : Filter or None, optional
|
|
827
|
+
Who is eligible. ``None`` uses every finite value. Default is ``None``.
|
|
828
|
+
groupby : Classifier or None, optional
|
|
829
|
+
Group codes. ``None`` is one group. ``-1`` is missing and is dropped. Default is ``None``.
|
|
830
|
+
|
|
831
|
+
Returns
|
|
832
|
+
-------
|
|
833
|
+
Factor
|
|
834
|
+
``x - group mean``. A group of one becomes 0.
|
|
835
|
+
"""
|
|
836
|
+
return CrossSection(self, "demean", mask, groupby)
|
|
837
|
+
|
|
838
|
+
def top(self, n: int, *, mask: Filter | None = None, groupby: Classifier | None = None) -> Filter:
|
|
839
|
+
"""Keep the ``n`` largest values.
|
|
840
|
+
|
|
841
|
+
Parameters
|
|
842
|
+
----------
|
|
843
|
+
n : int
|
|
844
|
+
How many names to keep, counted inside each group when ``groupby`` is set.
|
|
845
|
+
mask : Filter or None, optional
|
|
846
|
+
Who is eligible. ``None`` uses every finite value. Default is ``None``.
|
|
847
|
+
groupby : Classifier or None, optional
|
|
848
|
+
Group codes. ``None`` is one group. ``-1`` is missing and is dropped. Default is ``None``.
|
|
849
|
+
|
|
850
|
+
Returns
|
|
851
|
+
-------
|
|
852
|
+
Filter
|
|
853
|
+
True for assets whose descending rank is at most ``n``.
|
|
854
|
+
"""
|
|
855
|
+
return self.rank(mask=mask, groupby=groupby, ascending=False) <= n
|
|
856
|
+
|
|
857
|
+
def bottom(self, n: int, *, mask: Filter | None = None, groupby: Classifier | None = None) -> Filter:
|
|
858
|
+
"""Keep the ``n`` smallest values.
|
|
859
|
+
|
|
860
|
+
Parameters
|
|
861
|
+
----------
|
|
862
|
+
n : int
|
|
863
|
+
How many names to keep, counted inside each group when ``groupby`` is set.
|
|
864
|
+
mask : Filter or None, optional
|
|
865
|
+
Who is eligible. ``None`` uses every finite value. Default is ``None``.
|
|
866
|
+
groupby : Classifier or None, optional
|
|
867
|
+
Group codes. ``None`` is one group. ``-1`` is missing and is dropped. Default is ``None``.
|
|
868
|
+
|
|
869
|
+
Returns
|
|
870
|
+
-------
|
|
871
|
+
Filter
|
|
872
|
+
True for assets whose ascending rank is at most ``n``.
|
|
873
|
+
"""
|
|
874
|
+
return self.rank(mask=mask, groupby=groupby, ascending=True) <= n
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
class Filter(Term):
|
|
878
|
+
"""A boolean expression. Missing means False, so a missing value fails a screen.
|
|
879
|
+
|
|
880
|
+
Notes
|
|
881
|
+
-----
|
|
882
|
+
``&``, ``|``, and ``~`` combine filters. A bare ``True`` or ``False`` is accepted as the other operand.
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
kind = "filter"
|
|
886
|
+
dtype = np.bool_
|
|
887
|
+
missing = False
|
|
888
|
+
|
|
889
|
+
def __and__(self, o):
|
|
890
|
+
"""Intersect with another filter.
|
|
891
|
+
|
|
892
|
+
Parameters
|
|
893
|
+
----------
|
|
894
|
+
o : Filter or bool
|
|
895
|
+
The other mask. A bool is broadcast to every asset and bar.
|
|
896
|
+
|
|
897
|
+
Returns
|
|
898
|
+
-------
|
|
899
|
+
Filter
|
|
900
|
+
Elementwise boolean combination.
|
|
901
|
+
|
|
902
|
+
Raises
|
|
903
|
+
------
|
|
904
|
+
TypeError
|
|
905
|
+
If ``o`` is not a filter or a bool.
|
|
906
|
+
"""
|
|
907
|
+
return Logic(np.logical_and, "&", self, _as_filter(o))
|
|
908
|
+
|
|
909
|
+
def __rand__(self, o):
|
|
910
|
+
"""Intersect, with this filter on the right.
|
|
911
|
+
|
|
912
|
+
Parameters
|
|
913
|
+
----------
|
|
914
|
+
o : Filter or bool
|
|
915
|
+
The other mask. A bool is broadcast to every asset and bar.
|
|
916
|
+
|
|
917
|
+
Returns
|
|
918
|
+
-------
|
|
919
|
+
Filter
|
|
920
|
+
Elementwise boolean combination.
|
|
921
|
+
|
|
922
|
+
Raises
|
|
923
|
+
------
|
|
924
|
+
TypeError
|
|
925
|
+
If ``o`` is not a filter or a bool.
|
|
926
|
+
"""
|
|
927
|
+
return Logic(np.logical_and, "&", _as_filter(o), self)
|
|
928
|
+
|
|
929
|
+
def __or__(self, o):
|
|
930
|
+
"""Union with another filter.
|
|
931
|
+
|
|
932
|
+
Parameters
|
|
933
|
+
----------
|
|
934
|
+
o : Filter or bool
|
|
935
|
+
The other mask. A bool is broadcast to every asset and bar.
|
|
936
|
+
|
|
937
|
+
Returns
|
|
938
|
+
-------
|
|
939
|
+
Filter
|
|
940
|
+
Elementwise boolean combination.
|
|
941
|
+
|
|
942
|
+
Raises
|
|
943
|
+
------
|
|
944
|
+
TypeError
|
|
945
|
+
If ``o`` is not a filter or a bool.
|
|
946
|
+
"""
|
|
947
|
+
return Logic(np.logical_or, "|", self, _as_filter(o))
|
|
948
|
+
|
|
949
|
+
def __ror__(self, o):
|
|
950
|
+
"""Union, with this filter on the right.
|
|
951
|
+
|
|
952
|
+
Parameters
|
|
953
|
+
----------
|
|
954
|
+
o : Filter or bool
|
|
955
|
+
The other mask. A bool is broadcast to every asset and bar.
|
|
956
|
+
|
|
957
|
+
Returns
|
|
958
|
+
-------
|
|
959
|
+
Filter
|
|
960
|
+
Elementwise boolean combination.
|
|
961
|
+
|
|
962
|
+
Raises
|
|
963
|
+
------
|
|
964
|
+
TypeError
|
|
965
|
+
If ``o`` is not a filter or a bool.
|
|
966
|
+
"""
|
|
967
|
+
return Logic(np.logical_or, "|", _as_filter(o), self)
|
|
968
|
+
|
|
969
|
+
def __invert__(self):
|
|
970
|
+
"""Invert the filter.
|
|
971
|
+
|
|
972
|
+
Returns
|
|
973
|
+
-------
|
|
974
|
+
Filter
|
|
975
|
+
True where this filter is False, including where it was missing.
|
|
976
|
+
"""
|
|
977
|
+
return Not(self)
|
|
978
|
+
|
|
979
|
+
def shift(self, n: int = 1) -> Filter:
|
|
980
|
+
"""Lag the filter by ``n`` bars.
|
|
981
|
+
|
|
982
|
+
Parameters
|
|
983
|
+
----------
|
|
984
|
+
n : int, optional
|
|
985
|
+
Bars to look back. Default is 1. The first ``n`` rows are False.
|
|
986
|
+
|
|
987
|
+
Returns
|
|
988
|
+
-------
|
|
989
|
+
Filter
|
|
990
|
+
This filter from ``n`` bars earlier.
|
|
991
|
+
|
|
992
|
+
Raises
|
|
993
|
+
------
|
|
994
|
+
ValueError
|
|
995
|
+
If ``n < 1``.
|
|
996
|
+
"""
|
|
997
|
+
return ShiftFilter(self, n)
|
|
998
|
+
|
|
999
|
+
__hash__ = Term.__hash__
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
class Classifier(Term):
|
|
1003
|
+
"""Integer group codes. ``-1`` is missing and is left out of grouped ranks and z-scores.
|
|
1004
|
+
|
|
1005
|
+
Notes
|
|
1006
|
+
-----
|
|
1007
|
+
Build one with ``Column('sector', Classifier)``. Codes are whatever the caller stored in the panel.
|
|
1008
|
+
"""
|
|
1009
|
+
|
|
1010
|
+
kind = "classifier"
|
|
1011
|
+
dtype = np.int64
|
|
1012
|
+
missing = -1
|
|
1013
|
+
|
|
1014
|
+
def eq(self, code: int) -> Filter:
|
|
1015
|
+
"""Test membership in one code.
|
|
1016
|
+
|
|
1017
|
+
Parameters
|
|
1018
|
+
----------
|
|
1019
|
+
code : int
|
|
1020
|
+
Group code to match.
|
|
1021
|
+
|
|
1022
|
+
Returns
|
|
1023
|
+
-------
|
|
1024
|
+
Filter
|
|
1025
|
+
True where the classifier equals ``code``.
|
|
1026
|
+
"""
|
|
1027
|
+
return ClassifierEq(self, int(code))
|
|
1028
|
+
|
|
1029
|
+
def isnull(self) -> Filter:
|
|
1030
|
+
"""Test for a missing group code.
|
|
1031
|
+
|
|
1032
|
+
Returns
|
|
1033
|
+
-------
|
|
1034
|
+
Filter
|
|
1035
|
+
True where the code is ``-1``.
|
|
1036
|
+
"""
|
|
1037
|
+
return ClassifierEq(self, -1)
|
|
1038
|
+
|
|
1039
|
+
__hash__ = Term.__hash__
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _as_filter(o) -> Filter:
|
|
1043
|
+
if isinstance(o, Filter):
|
|
1044
|
+
return o
|
|
1045
|
+
if isinstance(o, bool):
|
|
1046
|
+
return ConstFilter(o)
|
|
1047
|
+
raise TypeError(f"expected a Filter, got {type(o).__name__}")
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
# ====================================================================== leaves
|
|
1051
|
+
class Column:
|
|
1052
|
+
"""Factory for input columns: ``Column("close")``, ``Column("sector", Classifier)``."""
|
|
1053
|
+
|
|
1054
|
+
_cache: ClassVar[dict[tuple[str, str], Term]] = {}
|
|
1055
|
+
|
|
1056
|
+
def __new__(cls, name: str, kind: type[Term] = Factor) -> Any: # type: ignore[misc]
|
|
1057
|
+
"""Return the input term for a panel column.
|
|
1058
|
+
|
|
1059
|
+
Parameters
|
|
1060
|
+
----------
|
|
1061
|
+
cls : type
|
|
1062
|
+
Ignored. ``Column`` is a factory, not a container.
|
|
1063
|
+
name : str
|
|
1064
|
+
Column name in the :class:`~factorflow.panel.Panel`.
|
|
1065
|
+
kind : type, optional
|
|
1066
|
+
:class:`Factor` (default), :class:`Filter`, or :class:`Classifier`.
|
|
1067
|
+
|
|
1068
|
+
Returns
|
|
1069
|
+
-------
|
|
1070
|
+
Term
|
|
1071
|
+
The same node every time for a given ``(name, kind)``.
|
|
1072
|
+
|
|
1073
|
+
Raises
|
|
1074
|
+
------
|
|
1075
|
+
KeyError
|
|
1076
|
+
If ``kind`` is not one of those three classes.
|
|
1077
|
+
"""
|
|
1078
|
+
key = (name, kind.__name__)
|
|
1079
|
+
if key not in cls._cache:
|
|
1080
|
+
node_cls = {Factor: FactorInput, Filter: FilterInput, Classifier: ClassifierInput}[kind]
|
|
1081
|
+
cls._cache[key] = node_cls(name)
|
|
1082
|
+
return cls._cache[key]
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
class _Input:
|
|
1086
|
+
window = 1
|
|
1087
|
+
is_input = True
|
|
1088
|
+
|
|
1089
|
+
def __init__(self, name: str):
|
|
1090
|
+
"""Remember the column name.
|
|
1091
|
+
|
|
1092
|
+
Parameters
|
|
1093
|
+
----------
|
|
1094
|
+
name : str
|
|
1095
|
+
Panel column this leaf reads.
|
|
1096
|
+
"""
|
|
1097
|
+
self.name = name
|
|
1098
|
+
self.params = (name,)
|
|
1099
|
+
|
|
1100
|
+
def __repr__(self) -> str:
|
|
1101
|
+
"""Return ``kind:name``, for example ``factor:close``.
|
|
1102
|
+
|
|
1103
|
+
Returns
|
|
1104
|
+
-------
|
|
1105
|
+
str
|
|
1106
|
+
A short label.
|
|
1107
|
+
"""
|
|
1108
|
+
return f"{self.kind}:{self.name}" # type: ignore[attr-defined]
|
|
1109
|
+
|
|
1110
|
+
def compute(self, blocks, prev=None): # inputs are filled by the executor
|
|
1111
|
+
"""Reject evaluation. The executors fill input terms from the panel.
|
|
1112
|
+
|
|
1113
|
+
Raises
|
|
1114
|
+
------
|
|
1115
|
+
RuntimeError
|
|
1116
|
+
Always. Inputs are not computed.
|
|
1117
|
+
"""
|
|
1118
|
+
raise RuntimeError("input terms are not computed")
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
class FactorInput(_Input, Factor):
|
|
1122
|
+
"""A float input column. Created by :class:`Column`, not called directly."""
|
|
1123
|
+
|
|
1124
|
+
__hash__ = Term.__hash__
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
class FilterInput(_Input, Filter):
|
|
1128
|
+
"""A boolean input column. Created by :class:`Column`, not called directly."""
|
|
1129
|
+
|
|
1130
|
+
__hash__ = Term.__hash__
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
class ClassifierInput(_Input, Classifier):
|
|
1134
|
+
"""An integer input column. Created by :class:`Column`, not called directly."""
|
|
1135
|
+
|
|
1136
|
+
__hash__ = Term.__hash__
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
class BarData:
|
|
1140
|
+
"""The standard bar fields produced by the bar builders."""
|
|
1141
|
+
|
|
1142
|
+
open: Factor = Column("open")
|
|
1143
|
+
high: Factor = Column("high")
|
|
1144
|
+
low: Factor = Column("low")
|
|
1145
|
+
close: Factor = Column("close")
|
|
1146
|
+
volume: Factor = Column("volume")
|
|
1147
|
+
dollar: Factor = Column("dollar")
|
|
1148
|
+
n_ticks: Factor = Column("n_ticks")
|
|
1149
|
+
has_trade: Filter = Column("has_trade", Filter)
|
|
1150
|
+
FIELDS: ClassVar[tuple[str, ...]] = ("open", "high", "low", "close", "volume", "dollar", "n_ticks")
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
class Constant(Factor):
|
|
1154
|
+
"""A scalar broadcast to every asset (it has no inputs and never changes)."""
|
|
1155
|
+
|
|
1156
|
+
def __init__(self, value: Scalar):
|
|
1157
|
+
"""Store a scalar that will be broadcast.
|
|
1158
|
+
|
|
1159
|
+
Parameters
|
|
1160
|
+
----------
|
|
1161
|
+
value : int or float or bool
|
|
1162
|
+
The constant. Stored as float.
|
|
1163
|
+
"""
|
|
1164
|
+
self.value = float(value)
|
|
1165
|
+
self.params = (self.value,)
|
|
1166
|
+
|
|
1167
|
+
__hash__ = Term.__hash__
|
|
1168
|
+
|
|
1169
|
+
def compute(self, blocks, prev=None):
|
|
1170
|
+
"""Reject evaluation. Constants are folded into the parent operator.
|
|
1171
|
+
|
|
1172
|
+
Raises
|
|
1173
|
+
------
|
|
1174
|
+
RuntimeError
|
|
1175
|
+
Always.
|
|
1176
|
+
"""
|
|
1177
|
+
raise RuntimeError("constants are folded into their parents")
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
class ConstFilter(Filter):
|
|
1181
|
+
"""A broadcast boolean. Built when a filter is combined with ``True`` or ``False``."""
|
|
1182
|
+
|
|
1183
|
+
def __init__(self, value: bool):
|
|
1184
|
+
"""Store the boolean.
|
|
1185
|
+
|
|
1186
|
+
Parameters
|
|
1187
|
+
----------
|
|
1188
|
+
value : bool
|
|
1189
|
+
The constant mask.
|
|
1190
|
+
"""
|
|
1191
|
+
self.value = bool(value)
|
|
1192
|
+
self.params = (self.value,)
|
|
1193
|
+
|
|
1194
|
+
__hash__ = Term.__hash__
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _operand(t: Term, blocks: Sequence[np.ndarray], i: int):
|
|
1198
|
+
return t.value if isinstance(t, (Constant, ConstFilter)) else blocks[i]
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _real_inputs(*ts: Term) -> tuple[Term, ...]:
|
|
1202
|
+
return tuple(t for t in ts if not isinstance(t, (Constant, ConstFilter)))
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
# ====================================================================== elementwise
|
|
1206
|
+
class _Elementwise:
|
|
1207
|
+
"""Mixin: elementwise over inputs; constants are folded."""
|
|
1208
|
+
|
|
1209
|
+
operands: tuple[Term, ...]
|
|
1210
|
+
|
|
1211
|
+
def _args(self, blocks: Sequence[np.ndarray]):
|
|
1212
|
+
it = iter(blocks)
|
|
1213
|
+
return [t.value if isinstance(t, (Constant, ConstFilter)) else next(it) for t in self.operands]
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
class BinOp(_Elementwise, Factor):
|
|
1217
|
+
"""Elementwise arithmetic node. Built by :class:`Factor` operators, not called directly."""
|
|
1218
|
+
|
|
1219
|
+
def __init__(self, fn: Callable, sym: str, a: Term, b: Term):
|
|
1220
|
+
"""Bind the operator and its two operands.
|
|
1221
|
+
|
|
1222
|
+
Parameters
|
|
1223
|
+
----------
|
|
1224
|
+
fn : callable
|
|
1225
|
+
The numpy or operator function.
|
|
1226
|
+
sym : str
|
|
1227
|
+
Symbol stored in the term identity, such as ``'+'``.
|
|
1228
|
+
a : Term
|
|
1229
|
+
Left operand. A :class:`Constant` is folded in and is not an input.
|
|
1230
|
+
b : Term
|
|
1231
|
+
Right operand.
|
|
1232
|
+
|
|
1233
|
+
Raises
|
|
1234
|
+
------
|
|
1235
|
+
TypeError
|
|
1236
|
+
If both operands are constants. Fold those before building a node.
|
|
1237
|
+
"""
|
|
1238
|
+
if isinstance(a, Constant) and isinstance(b, Constant):
|
|
1239
|
+
raise TypeError("both operands are constants")
|
|
1240
|
+
self.fn, self.operands = fn, (a, b)
|
|
1241
|
+
self.inputs = _real_inputs(a, b)
|
|
1242
|
+
self.params = (sym, tuple(t.params if isinstance(t, Constant) else ("in",) for t in (a, b)))
|
|
1243
|
+
|
|
1244
|
+
__hash__ = Term.__hash__
|
|
1245
|
+
|
|
1246
|
+
def compute(self, blocks, prev=None):
|
|
1247
|
+
"""Apply the operator.
|
|
1248
|
+
|
|
1249
|
+
Parameters
|
|
1250
|
+
----------
|
|
1251
|
+
blocks : sequence of ndarray
|
|
1252
|
+
Inputs that are not constants, in input order. Shape ``(T, N)``.
|
|
1253
|
+
prev : ndarray or None
|
|
1254
|
+
Ignored.
|
|
1255
|
+
|
|
1256
|
+
Returns
|
|
1257
|
+
-------
|
|
1258
|
+
ndarray
|
|
1259
|
+
Float64 result. Divide-by-zero and overflow become NaN, not exceptions.
|
|
1260
|
+
"""
|
|
1261
|
+
a, b = self._args(blocks)
|
|
1262
|
+
with np.errstate(invalid="ignore", divide="ignore", over="ignore"):
|
|
1263
|
+
return np.asarray(self.fn(a, b), dtype=np.float64)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
class Unary(Factor):
|
|
1267
|
+
"""Elementwise unary node, such as ``log``, ``sqrt``, ``neg``, or ``abs``."""
|
|
1268
|
+
|
|
1269
|
+
def __init__(self, fn: Callable, name: str, a: Factor, columnwise: bool = True):
|
|
1270
|
+
"""Bind the function.
|
|
1271
|
+
|
|
1272
|
+
Parameters
|
|
1273
|
+
----------
|
|
1274
|
+
fn : callable
|
|
1275
|
+
A numpy ufunc.
|
|
1276
|
+
name : str
|
|
1277
|
+
Name stored in the term identity.
|
|
1278
|
+
a : Factor
|
|
1279
|
+
The input.
|
|
1280
|
+
columnwise : bool, optional
|
|
1281
|
+
``False`` for ``log``, which is always computed on full rows so
|
|
1282
|
+
live and batch take the same path. Default is ``True``.
|
|
1283
|
+
"""
|
|
1284
|
+
self.fn, self.inputs, self.params = fn, (a,), (name,)
|
|
1285
|
+
# transcendental ufuncs may take different SIMD paths for different array lengths,
|
|
1286
|
+
# so they are always computed on full rows (never on a column subset).
|
|
1287
|
+
self.columnwise = columnwise
|
|
1288
|
+
self.rowwise = not columnwise
|
|
1289
|
+
|
|
1290
|
+
__hash__ = Term.__hash__
|
|
1291
|
+
|
|
1292
|
+
def compute(self, blocks, prev=None):
|
|
1293
|
+
"""Apply the ufunc.
|
|
1294
|
+
|
|
1295
|
+
Parameters
|
|
1296
|
+
----------
|
|
1297
|
+
blocks : sequence of ndarray
|
|
1298
|
+
The single input.
|
|
1299
|
+
prev : ndarray or None
|
|
1300
|
+
Ignored.
|
|
1301
|
+
|
|
1302
|
+
Returns
|
|
1303
|
+
-------
|
|
1304
|
+
ndarray
|
|
1305
|
+
Float64 result. Domain errors become NaN.
|
|
1306
|
+
"""
|
|
1307
|
+
with np.errstate(invalid="ignore", divide="ignore", over="ignore"):
|
|
1308
|
+
return np.asarray(self.fn(blocks[0]), dtype=np.float64)
|
|
1309
|
+
|
|
1310
|
+
def compute_row(self, rows, prev):
|
|
1311
|
+
"""Apply the ufunc to the last row. Used when ``columnwise`` is False.
|
|
1312
|
+
|
|
1313
|
+
Parameters
|
|
1314
|
+
----------
|
|
1315
|
+
rows : sequence of ndarray
|
|
1316
|
+
Windows; the last row is evaluated.
|
|
1317
|
+
prev : ndarray or None
|
|
1318
|
+
Ignored.
|
|
1319
|
+
|
|
1320
|
+
Returns
|
|
1321
|
+
-------
|
|
1322
|
+
ndarray of shape (N,)
|
|
1323
|
+
One row.
|
|
1324
|
+
"""
|
|
1325
|
+
return self.compute([r[-1] for r in rows])
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
class FillNa(Factor):
|
|
1329
|
+
"""Replace NaN with a constant. Built by :meth:`Factor.fillna`."""
|
|
1330
|
+
|
|
1331
|
+
def __init__(self, a: Factor, value: float):
|
|
1332
|
+
"""Store the input and the fill value.
|
|
1333
|
+
|
|
1334
|
+
Parameters
|
|
1335
|
+
----------
|
|
1336
|
+
a : Factor
|
|
1337
|
+
The input.
|
|
1338
|
+
value : float
|
|
1339
|
+
Replacement for NaN.
|
|
1340
|
+
"""
|
|
1341
|
+
self.inputs, self.params, self.value = (a,), (value,), value
|
|
1342
|
+
|
|
1343
|
+
__hash__ = Term.__hash__
|
|
1344
|
+
|
|
1345
|
+
def compute(self, blocks, prev=None):
|
|
1346
|
+
"""Replace NaNs.
|
|
1347
|
+
|
|
1348
|
+
Parameters
|
|
1349
|
+
----------
|
|
1350
|
+
blocks : sequence of ndarray
|
|
1351
|
+
The input.
|
|
1352
|
+
prev : ndarray or None
|
|
1353
|
+
Ignored.
|
|
1354
|
+
|
|
1355
|
+
Returns
|
|
1356
|
+
-------
|
|
1357
|
+
ndarray
|
|
1358
|
+
``value`` where the input is NaN, otherwise the input.
|
|
1359
|
+
"""
|
|
1360
|
+
x = blocks[0]
|
|
1361
|
+
return np.where(np.isnan(x), self.value, x)
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
class Where(_Elementwise, Factor):
|
|
1365
|
+
"""Conditional choice. Built by :func:`where`."""
|
|
1366
|
+
|
|
1367
|
+
def __init__(self, cond: Filter, a: Factor | Scalar, b: Factor | Scalar):
|
|
1368
|
+
"""Bind the condition and the two branches.
|
|
1369
|
+
|
|
1370
|
+
Parameters
|
|
1371
|
+
----------
|
|
1372
|
+
cond : Filter
|
|
1373
|
+
Which branch to take.
|
|
1374
|
+
a : Factor or scalar
|
|
1375
|
+
Value where ``cond`` is True.
|
|
1376
|
+
b : Factor or scalar
|
|
1377
|
+
Value where ``cond`` is False.
|
|
1378
|
+
"""
|
|
1379
|
+
la, lb = _lift(a), _lift(b)
|
|
1380
|
+
self.operands = (cond, la, lb)
|
|
1381
|
+
self.inputs = _real_inputs(cond, la, lb)
|
|
1382
|
+
self.params = tuple(t.params if isinstance(t, Constant) else ("in",) for t in (la, lb))
|
|
1383
|
+
|
|
1384
|
+
__hash__ = Term.__hash__
|
|
1385
|
+
|
|
1386
|
+
def compute(self, blocks, prev=None):
|
|
1387
|
+
"""Select elements.
|
|
1388
|
+
|
|
1389
|
+
Parameters
|
|
1390
|
+
----------
|
|
1391
|
+
blocks : sequence of ndarray
|
|
1392
|
+
Non-constant operands.
|
|
1393
|
+
prev : ndarray or None
|
|
1394
|
+
Ignored.
|
|
1395
|
+
|
|
1396
|
+
Returns
|
|
1397
|
+
-------
|
|
1398
|
+
ndarray
|
|
1399
|
+
Float64 ``np.where`` result.
|
|
1400
|
+
"""
|
|
1401
|
+
c, a, b = self._args(blocks)
|
|
1402
|
+
return np.where(c, a, b).astype(np.float64)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def where(cond: Filter, a: Factor | Scalar, b: Factor | Scalar) -> Factor:
|
|
1406
|
+
"""Choose ``a`` or ``b`` per element.
|
|
1407
|
+
|
|
1408
|
+
Parameters
|
|
1409
|
+
----------
|
|
1410
|
+
cond : Filter
|
|
1411
|
+
Where to take ``a``. False, including a missing filter, takes ``b``.
|
|
1412
|
+
a : Factor or int or float or bool
|
|
1413
|
+
Value where ``cond`` is True.
|
|
1414
|
+
b : Factor or int or float or bool
|
|
1415
|
+
Value where ``cond`` is False.
|
|
1416
|
+
|
|
1417
|
+
Returns
|
|
1418
|
+
-------
|
|
1419
|
+
Factor
|
|
1420
|
+
``a`` or ``b``, as float64.
|
|
1421
|
+
|
|
1422
|
+
Examples
|
|
1423
|
+
--------
|
|
1424
|
+
>>> import factorflow as ff
|
|
1425
|
+
>>> ff.where(ff.Column('close') > 0, 1, 0).kind
|
|
1426
|
+
'factor'
|
|
1427
|
+
"""
|
|
1428
|
+
return Where(cond, a, b)
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
class Compare(_Elementwise, Filter):
|
|
1432
|
+
"""Elementwise comparison. Built by :class:`Factor` comparisons."""
|
|
1433
|
+
|
|
1434
|
+
def __init__(self, fn: Callable, sym: str, a: Term, b: Term):
|
|
1435
|
+
"""Bind the comparison.
|
|
1436
|
+
|
|
1437
|
+
Parameters
|
|
1438
|
+
----------
|
|
1439
|
+
fn : callable
|
|
1440
|
+
The comparison function.
|
|
1441
|
+
sym : str
|
|
1442
|
+
Symbol stored in the identity, such as ``'<'``.
|
|
1443
|
+
a : Term
|
|
1444
|
+
Left side.
|
|
1445
|
+
b : Term
|
|
1446
|
+
Right side.
|
|
1447
|
+
"""
|
|
1448
|
+
self.fn, self.operands = fn, (a, b)
|
|
1449
|
+
self.inputs = _real_inputs(a, b)
|
|
1450
|
+
self.params = (sym, tuple(t.params if isinstance(t, Constant) else ("in",) for t in (a, b)))
|
|
1451
|
+
|
|
1452
|
+
__hash__ = Term.__hash__
|
|
1453
|
+
|
|
1454
|
+
def compute(self, blocks, prev=None):
|
|
1455
|
+
"""Compare the operands.
|
|
1456
|
+
|
|
1457
|
+
Parameters
|
|
1458
|
+
----------
|
|
1459
|
+
blocks : sequence of ndarray
|
|
1460
|
+
Non-constant operands.
|
|
1461
|
+
prev : ndarray or None
|
|
1462
|
+
Ignored.
|
|
1463
|
+
|
|
1464
|
+
Returns
|
|
1465
|
+
-------
|
|
1466
|
+
ndarray of bool
|
|
1467
|
+
False where a comparison involves NaN.
|
|
1468
|
+
"""
|
|
1469
|
+
a, b = self._args(blocks)
|
|
1470
|
+
with np.errstate(invalid="ignore"):
|
|
1471
|
+
return np.asarray(self.fn(a, b), dtype=np.bool_)
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
class Logic(_Elementwise, Filter):
|
|
1475
|
+
"""Elementwise ``and`` or ``or`` of two filters."""
|
|
1476
|
+
|
|
1477
|
+
def __init__(self, fn: Callable, sym: str, a: Filter, b: Filter):
|
|
1478
|
+
"""Bind the boolean operator.
|
|
1479
|
+
|
|
1480
|
+
Parameters
|
|
1481
|
+
----------
|
|
1482
|
+
fn : callable
|
|
1483
|
+
``np.logical_and`` or ``np.logical_or``.
|
|
1484
|
+
sym : str
|
|
1485
|
+
``'&'`` or ``'|'``.
|
|
1486
|
+
a : Filter
|
|
1487
|
+
Left filter.
|
|
1488
|
+
b : Filter
|
|
1489
|
+
Right filter.
|
|
1490
|
+
"""
|
|
1491
|
+
self.fn, self.operands = fn, (a, b)
|
|
1492
|
+
self.inputs = _real_inputs(a, b)
|
|
1493
|
+
self.params = (sym, tuple(t.params if isinstance(t, ConstFilter) else ("in",) for t in (a, b)))
|
|
1494
|
+
|
|
1495
|
+
__hash__ = Term.__hash__
|
|
1496
|
+
|
|
1497
|
+
def compute(self, blocks, prev=None):
|
|
1498
|
+
"""Combine the filters.
|
|
1499
|
+
|
|
1500
|
+
Parameters
|
|
1501
|
+
----------
|
|
1502
|
+
blocks : sequence of ndarray
|
|
1503
|
+
Non-constant operands.
|
|
1504
|
+
prev : ndarray or None
|
|
1505
|
+
Ignored.
|
|
1506
|
+
|
|
1507
|
+
Returns
|
|
1508
|
+
-------
|
|
1509
|
+
ndarray of bool
|
|
1510
|
+
The combination.
|
|
1511
|
+
"""
|
|
1512
|
+
a, b = self._args(blocks)
|
|
1513
|
+
return np.asarray(self.fn(a, b), dtype=np.bool_)
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
class Not(Filter):
|
|
1517
|
+
"""Negation of a filter. Built by ``~``."""
|
|
1518
|
+
|
|
1519
|
+
def __init__(self, a: Filter):
|
|
1520
|
+
"""Store the filter to invert.
|
|
1521
|
+
|
|
1522
|
+
Parameters
|
|
1523
|
+
----------
|
|
1524
|
+
a : Filter
|
|
1525
|
+
The input.
|
|
1526
|
+
"""
|
|
1527
|
+
self.inputs = (a,)
|
|
1528
|
+
|
|
1529
|
+
__hash__ = Term.__hash__
|
|
1530
|
+
|
|
1531
|
+
def compute(self, blocks, prev=None):
|
|
1532
|
+
"""Invert the mask.
|
|
1533
|
+
|
|
1534
|
+
Parameters
|
|
1535
|
+
----------
|
|
1536
|
+
blocks : sequence of ndarray
|
|
1537
|
+
The filter.
|
|
1538
|
+
prev : ndarray or None
|
|
1539
|
+
Ignored.
|
|
1540
|
+
|
|
1541
|
+
Returns
|
|
1542
|
+
-------
|
|
1543
|
+
ndarray of bool
|
|
1544
|
+
The logical not.
|
|
1545
|
+
"""
|
|
1546
|
+
return ~blocks[0]
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
class IsNull(Filter):
|
|
1550
|
+
"""NaN test. Built by :meth:`Factor.isnull`."""
|
|
1551
|
+
|
|
1552
|
+
def __init__(self, a: Factor):
|
|
1553
|
+
"""Store the factor to test.
|
|
1554
|
+
|
|
1555
|
+
Parameters
|
|
1556
|
+
----------
|
|
1557
|
+
a : Factor
|
|
1558
|
+
The input.
|
|
1559
|
+
"""
|
|
1560
|
+
self.inputs = (a,)
|
|
1561
|
+
|
|
1562
|
+
__hash__ = Term.__hash__
|
|
1563
|
+
|
|
1564
|
+
def compute(self, blocks, prev=None):
|
|
1565
|
+
"""Test for NaN.
|
|
1566
|
+
|
|
1567
|
+
Parameters
|
|
1568
|
+
----------
|
|
1569
|
+
blocks : sequence of ndarray
|
|
1570
|
+
The factor.
|
|
1571
|
+
prev : ndarray or None
|
|
1572
|
+
Ignored.
|
|
1573
|
+
|
|
1574
|
+
Returns
|
|
1575
|
+
-------
|
|
1576
|
+
ndarray of bool
|
|
1577
|
+
True where the input is NaN.
|
|
1578
|
+
"""
|
|
1579
|
+
return np.isnan(blocks[0])
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
class ClassifierEq(Filter):
|
|
1583
|
+
"""Membership test. Built by :meth:`Classifier.eq` and :meth:`Classifier.isnull`."""
|
|
1584
|
+
|
|
1585
|
+
def __init__(self, a: Classifier, code: int):
|
|
1586
|
+
"""Store the classifier and the code.
|
|
1587
|
+
|
|
1588
|
+
Parameters
|
|
1589
|
+
----------
|
|
1590
|
+
a : Classifier
|
|
1591
|
+
The input.
|
|
1592
|
+
code : int
|
|
1593
|
+
Code to match. ``-1`` tests for missing.
|
|
1594
|
+
"""
|
|
1595
|
+
self.inputs, self.params, self.code = (a,), (code,), code
|
|
1596
|
+
|
|
1597
|
+
__hash__ = Term.__hash__
|
|
1598
|
+
|
|
1599
|
+
def compute(self, blocks, prev=None):
|
|
1600
|
+
"""Compare codes.
|
|
1601
|
+
|
|
1602
|
+
Parameters
|
|
1603
|
+
----------
|
|
1604
|
+
blocks : sequence of ndarray
|
|
1605
|
+
The classifier, int64.
|
|
1606
|
+
prev : ndarray or None
|
|
1607
|
+
Ignored.
|
|
1608
|
+
|
|
1609
|
+
Returns
|
|
1610
|
+
-------
|
|
1611
|
+
ndarray of bool
|
|
1612
|
+
True where the code equals ``self.code``.
|
|
1613
|
+
"""
|
|
1614
|
+
return blocks[0] == self.code
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
# ====================================================================== time series
|
|
1618
|
+
_ROLLING = {
|
|
1619
|
+
"sum": K.wsum,
|
|
1620
|
+
"mean": K.wmean,
|
|
1621
|
+
"var": K.wvar,
|
|
1622
|
+
"std": K.wstd,
|
|
1623
|
+
"min": K.wmin,
|
|
1624
|
+
"max": K.wmax,
|
|
1625
|
+
"tsrank": K.wtsrank,
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
class Rolling(Factor):
|
|
1630
|
+
"""A one-input rolling window. Built by the ``rolling_*`` and :meth:`Factor.ts_rank` methods."""
|
|
1631
|
+
|
|
1632
|
+
def __init__(self, a: Factor, w: int, how: str):
|
|
1633
|
+
"""Store the input, the window, and which reduction to run.
|
|
1634
|
+
|
|
1635
|
+
Parameters
|
|
1636
|
+
----------
|
|
1637
|
+
a : Factor
|
|
1638
|
+
The input.
|
|
1639
|
+
w : int
|
|
1640
|
+
Window length in bars.
|
|
1641
|
+
how : str
|
|
1642
|
+
One of ``sum``, ``mean``, ``var``, ``std``, ``min``, ``max``, ``tsrank``.
|
|
1643
|
+
|
|
1644
|
+
Raises
|
|
1645
|
+
------
|
|
1646
|
+
ValueError
|
|
1647
|
+
If ``w < 1``, or if a variance or standard deviation has ``w < 2``.
|
|
1648
|
+
"""
|
|
1649
|
+
if w < 1:
|
|
1650
|
+
raise ValueError("window must be >= 1")
|
|
1651
|
+
if how in ("var", "std") and w < 2:
|
|
1652
|
+
raise ValueError(f"rolling_{how} needs a window >= 2")
|
|
1653
|
+
self.inputs, self.window, self.params, self.how = (a,), int(w), (how,), how
|
|
1654
|
+
|
|
1655
|
+
__hash__ = Term.__hash__
|
|
1656
|
+
|
|
1657
|
+
def compute(self, blocks, prev=None):
|
|
1658
|
+
"""Run the windowed kernel.
|
|
1659
|
+
|
|
1660
|
+
Parameters
|
|
1661
|
+
----------
|
|
1662
|
+
blocks : sequence of ndarray
|
|
1663
|
+
The input, shape ``(T_in, N)``.
|
|
1664
|
+
prev : ndarray or None
|
|
1665
|
+
Ignored.
|
|
1666
|
+
|
|
1667
|
+
Returns
|
|
1668
|
+
-------
|
|
1669
|
+
ndarray of shape (T_in - w + 1, N)
|
|
1670
|
+
The reduction. Same function batch and live call.
|
|
1671
|
+
"""
|
|
1672
|
+
return _ROLLING[self.how](blocks[0], self.window)
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
class Rolling2(Factor):
|
|
1676
|
+
"""A two-input rolling window: covariance or correlation."""
|
|
1677
|
+
|
|
1678
|
+
def __init__(self, a: Factor, b: Factor, w: int, how: str):
|
|
1679
|
+
"""Store the pair and the window.
|
|
1680
|
+
|
|
1681
|
+
Parameters
|
|
1682
|
+
----------
|
|
1683
|
+
a : Factor
|
|
1684
|
+
Left series.
|
|
1685
|
+
b : Factor
|
|
1686
|
+
Right series.
|
|
1687
|
+
w : int
|
|
1688
|
+
Window length in bars.
|
|
1689
|
+
how : str
|
|
1690
|
+
``cov`` or ``corr``.
|
|
1691
|
+
|
|
1692
|
+
Raises
|
|
1693
|
+
------
|
|
1694
|
+
ValueError
|
|
1695
|
+
If ``w < 2``.
|
|
1696
|
+
"""
|
|
1697
|
+
if w < 2:
|
|
1698
|
+
raise ValueError("a two-input rolling window must be >= 2")
|
|
1699
|
+
self.inputs, self.window, self.params, self.how = (a, b), int(w), (how,), how
|
|
1700
|
+
|
|
1701
|
+
__hash__ = Term.__hash__
|
|
1702
|
+
|
|
1703
|
+
def compute(self, blocks, prev=None):
|
|
1704
|
+
"""Run the two-series kernel.
|
|
1705
|
+
|
|
1706
|
+
Parameters
|
|
1707
|
+
----------
|
|
1708
|
+
blocks : sequence of ndarray
|
|
1709
|
+
The two inputs.
|
|
1710
|
+
prev : ndarray or None
|
|
1711
|
+
Ignored.
|
|
1712
|
+
|
|
1713
|
+
Returns
|
|
1714
|
+
-------
|
|
1715
|
+
ndarray
|
|
1716
|
+
Covariance or correlation, shape ``(T_in - w + 1, N)``.
|
|
1717
|
+
"""
|
|
1718
|
+
fn = K.wcov if self.how == "cov" else K.wcorr
|
|
1719
|
+
return fn(blocks[0], blocks[1], self.window)
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
class Shift(Factor):
|
|
1723
|
+
"""A lag of a factor. Built by :meth:`Factor.shift`."""
|
|
1724
|
+
|
|
1725
|
+
def __init__(self, a: Factor, n: int):
|
|
1726
|
+
"""Store the lag.
|
|
1727
|
+
|
|
1728
|
+
Parameters
|
|
1729
|
+
----------
|
|
1730
|
+
a : Factor
|
|
1731
|
+
The input.
|
|
1732
|
+
n : int
|
|
1733
|
+
Bars to look back. The kernel window is ``n + 1``.
|
|
1734
|
+
|
|
1735
|
+
Raises
|
|
1736
|
+
------
|
|
1737
|
+
ValueError
|
|
1738
|
+
If ``n < 1``.
|
|
1739
|
+
"""
|
|
1740
|
+
if n < 1:
|
|
1741
|
+
raise ValueError("shift must be >= 1")
|
|
1742
|
+
self.inputs, self.window, self.params = (a,), int(n) + 1, ()
|
|
1743
|
+
|
|
1744
|
+
__hash__ = Term.__hash__
|
|
1745
|
+
|
|
1746
|
+
def compute(self, blocks, prev=None):
|
|
1747
|
+
"""Drop the last ``n`` rows of the block so each output lines up with its lag.
|
|
1748
|
+
|
|
1749
|
+
Parameters
|
|
1750
|
+
----------
|
|
1751
|
+
blocks : sequence of ndarray
|
|
1752
|
+
The input.
|
|
1753
|
+
prev : ndarray or None
|
|
1754
|
+
Ignored.
|
|
1755
|
+
|
|
1756
|
+
Returns
|
|
1757
|
+
-------
|
|
1758
|
+
ndarray
|
|
1759
|
+
Shape ``(T_in - n, N)``.
|
|
1760
|
+
"""
|
|
1761
|
+
return K.wshift(blocks[0], self.window)
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
class ShiftFilter(Filter):
|
|
1765
|
+
"""A lag of a filter. Built by :meth:`Filter.shift`. The leading rows are False, not NaN."""
|
|
1766
|
+
|
|
1767
|
+
def __init__(self, a: Filter, n: int):
|
|
1768
|
+
"""Store the lag.
|
|
1769
|
+
|
|
1770
|
+
Parameters
|
|
1771
|
+
----------
|
|
1772
|
+
a : Filter
|
|
1773
|
+
The input.
|
|
1774
|
+
n : int
|
|
1775
|
+
Bars to look back.
|
|
1776
|
+
|
|
1777
|
+
Raises
|
|
1778
|
+
------
|
|
1779
|
+
ValueError
|
|
1780
|
+
If ``n < 1``.
|
|
1781
|
+
"""
|
|
1782
|
+
if n < 1:
|
|
1783
|
+
raise ValueError("shift must be >= 1")
|
|
1784
|
+
self.inputs, self.window, self.params = (a,), int(n) + 1, ()
|
|
1785
|
+
|
|
1786
|
+
__hash__ = Term.__hash__
|
|
1787
|
+
|
|
1788
|
+
def compute(self, blocks, prev=None):
|
|
1789
|
+
"""Lag the boolean block.
|
|
1790
|
+
|
|
1791
|
+
Parameters
|
|
1792
|
+
----------
|
|
1793
|
+
blocks : sequence of ndarray
|
|
1794
|
+
The filter.
|
|
1795
|
+
prev : ndarray or None
|
|
1796
|
+
Ignored.
|
|
1797
|
+
|
|
1798
|
+
Returns
|
|
1799
|
+
-------
|
|
1800
|
+
ndarray of bool
|
|
1801
|
+
The lagged mask. Padding in batch is the filter's missing value, False.
|
|
1802
|
+
"""
|
|
1803
|
+
return K.wshift(blocks[0], self.window).astype(np.bool_)
|
|
1804
|
+
|
|
1805
|
+
|
|
1806
|
+
class EWMMean(Factor):
|
|
1807
|
+
"""Exponentially weighted mean with a half-life in bars (recursive, O(1) per row)."""
|
|
1808
|
+
|
|
1809
|
+
rowwise = True
|
|
1810
|
+
recursive = True
|
|
1811
|
+
|
|
1812
|
+
def __init__(self, a: Factor, halflife: float):
|
|
1813
|
+
"""Store the half-life and the matching smoothing weight.
|
|
1814
|
+
|
|
1815
|
+
Parameters
|
|
1816
|
+
----------
|
|
1817
|
+
a : Factor
|
|
1818
|
+
The input.
|
|
1819
|
+
halflife : float
|
|
1820
|
+
Half-life in bars. Must be positive.
|
|
1821
|
+
|
|
1822
|
+
Raises
|
|
1823
|
+
------
|
|
1824
|
+
ValueError
|
|
1825
|
+
If ``halflife <= 0``.
|
|
1826
|
+
"""
|
|
1827
|
+
if halflife <= 0:
|
|
1828
|
+
raise ValueError("halflife must be > 0")
|
|
1829
|
+
self.inputs, self.params = (a,), (float(halflife),)
|
|
1830
|
+
self.alpha = K.ewm_alpha(float(halflife))
|
|
1831
|
+
|
|
1832
|
+
__hash__ = Term.__hash__
|
|
1833
|
+
|
|
1834
|
+
def compute_row(self, rows, prev):
|
|
1835
|
+
"""Advance the mean by one row.
|
|
1836
|
+
|
|
1837
|
+
Parameters
|
|
1838
|
+
----------
|
|
1839
|
+
rows : sequence of ndarray
|
|
1840
|
+
The input window. The last row is the new observation.
|
|
1841
|
+
prev : ndarray of shape (N,) or None
|
|
1842
|
+
Previous mean. ``None`` starts at the observation.
|
|
1843
|
+
|
|
1844
|
+
Returns
|
|
1845
|
+
-------
|
|
1846
|
+
ndarray of shape (N,)
|
|
1847
|
+
The updated mean. A NaN observation keeps ``prev``.
|
|
1848
|
+
"""
|
|
1849
|
+
p = prev if prev is not None else np.full(rows[0].shape[-1], np.nan)
|
|
1850
|
+
return K.ewm_step(rows[0][-1], p, self.alpha)
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
# ====================================================================== cross-sectional
|
|
1854
|
+
class CrossSection(Factor):
|
|
1855
|
+
"""Rank, z-score, or demean across assets.
|
|
1856
|
+
|
|
1857
|
+
Built by :meth:`Factor.rank`, :meth:`Factor.zscore`, and :meth:`Factor.demean`.
|
|
1858
|
+
|
|
1859
|
+
Notes
|
|
1860
|
+
-----
|
|
1861
|
+
``columnwise`` is False: one changed asset recomputes the whole row,
|
|
1862
|
+
because every output depends on the group.
|
|
1863
|
+
"""
|
|
1864
|
+
|
|
1865
|
+
rowwise = True
|
|
1866
|
+
columnwise = False
|
|
1867
|
+
|
|
1868
|
+
def __init__(
|
|
1869
|
+
self, a: Factor, how: str, mask: Filter | None, groupby: Classifier | None, ascending: bool = True
|
|
1870
|
+
):
|
|
1871
|
+
"""Bind the reduction and the optional mask and groups.
|
|
1872
|
+
|
|
1873
|
+
Parameters
|
|
1874
|
+
----------
|
|
1875
|
+
a : Factor
|
|
1876
|
+
Values to reduce.
|
|
1877
|
+
how : str
|
|
1878
|
+
``rank``, ``zscore``, or ``demean``.
|
|
1879
|
+
mask : Filter or None
|
|
1880
|
+
Who is eligible.
|
|
1881
|
+
groupby : Classifier or None
|
|
1882
|
+
Group codes.
|
|
1883
|
+
ascending : bool, optional
|
|
1884
|
+
Rank direction. Ignored by z-score and demean. Default is ``True``.
|
|
1885
|
+
"""
|
|
1886
|
+
self.how, self.ascending = how, ascending
|
|
1887
|
+
self.has_mask, self.has_groups = mask is not None, groupby is not None
|
|
1888
|
+
ins: list[Term] = [a]
|
|
1889
|
+
if mask is not None:
|
|
1890
|
+
ins.append(mask)
|
|
1891
|
+
if groupby is not None:
|
|
1892
|
+
ins.append(groupby)
|
|
1893
|
+
self.inputs = tuple(ins)
|
|
1894
|
+
self.params = (how, ascending, self.has_mask, self.has_groups)
|
|
1895
|
+
|
|
1896
|
+
__hash__ = Term.__hash__
|
|
1897
|
+
|
|
1898
|
+
def compute_row(self, rows, prev):
|
|
1899
|
+
"""Reduce one cross-section.
|
|
1900
|
+
|
|
1901
|
+
Parameters
|
|
1902
|
+
----------
|
|
1903
|
+
rows : sequence of ndarray
|
|
1904
|
+
Value, then the mask if any, then the groups if any. The last row of each is used.
|
|
1905
|
+
prev : ndarray or None
|
|
1906
|
+
Ignored. Cross-sections have no memory.
|
|
1907
|
+
|
|
1908
|
+
Returns
|
|
1909
|
+
-------
|
|
1910
|
+
ndarray of shape (N,)
|
|
1911
|
+
The rank, z-score, or demeaned value. Ineligible assets are NaN.
|
|
1912
|
+
"""
|
|
1913
|
+
it = iter(r[-1] for r in rows)
|
|
1914
|
+
v = next(it)
|
|
1915
|
+
mask = next(it) if self.has_mask else None
|
|
1916
|
+
groups = next(it) if self.has_groups else None
|
|
1917
|
+
if self.how == "rank":
|
|
1918
|
+
return K.cs_rank(v, mask, groups, self.ascending)
|
|
1919
|
+
if self.how == "zscore":
|
|
1920
|
+
return K.cs_zscore(v, mask, groups)
|
|
1921
|
+
return K.cs_demean(v, mask, groups)
|