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/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)