Completed
Pull Request — master (#910)
by
unknown
01:20
created

from_halflife()   A

Complexity

Conditions 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 20
rs 9.4286
1
"""
2
Technical Analysis Factors
3
--------------------------
4
"""
5
from bottleneck import (
6
    nanargmax,
7
    nanmax,
8
    nanmean,
9
    nansum,
10
)
11
from numbers import Number
12
from numpy import (
13
    abs,
14
    arange,
15
    average,
16
    clip,
17
    diff,
18
    exp,
19
    fmax,
20
    full,
21
    inf,
22
    isnan,
23
    log,
24
    NINF,
25
    sqrt,
26
    sum as np_sum,
27
)
28
from numexpr import evaluate
29
30
from zipline.pipeline.data import USEquityPricing
31
from zipline.pipeline.mixins import SingleInputMixin
32
from zipline.utils.control_flow import ignore_nanwarnings
33
from zipline.utils.input_validation import expect_types
34
from .factor import CustomFactor
35
36
37
class Returns(CustomFactor):
38
    """
39
    Calculates the percent change in close price over the given window_length.
40
41
    **Default Inputs**: [USEquityPricing.close]
42
    """
43
    inputs = [USEquityPricing.close]
44
45
    def compute(self, today, assets, out, close):
46
        out[:] = (close[-1] - close[0]) / close[0]
47
48
49
class RSI(CustomFactor, SingleInputMixin):
50
    """
51
    Relative Strength Index
52
53
    **Default Inputs**: [USEquityPricing.close]
54
55
    **Default Window Length**: 15
56
    """
57
    window_length = 15
58
    inputs = (USEquityPricing.close,)
59
60
    def compute(self, today, assets, out, closes):
61
        diffs = diff(closes, axis=0)
62
        ups = nanmean(clip(diffs, 0, inf), axis=0)
63
        downs = abs(nanmean(clip(diffs, -inf, 0), axis=0))
64
        return evaluate(
65
            "100 - (100 / (1 + (ups / downs)))",
66
            local_dict={'ups': ups, 'downs': downs},
67
            global_dict={},
68
            out=out,
69
        )
70
71
72
class SimpleMovingAverage(CustomFactor, SingleInputMixin):
73
    """
74
    Average Value of an arbitrary column
75
76
    **Default Inputs**: None
77
78
    **Default Window Length**: None
79
    """
80
    # numpy's nan functions throw warnings when passed an array containing only
81
    # nans, but they still returns the desired value (nan), so we ignore the
82
    # warning.
83
    ctx = ignore_nanwarnings()
84
85
    def compute(self, today, assets, out, data):
86
        out[:] = nanmean(data, axis=0)
87
88
89
class WeightedAverageValue(CustomFactor):
90
    """
91
    Helper for VWAP-like computations.
92
93
    **Default Inputs:** None
94
95
    **Default Window Length:** None
96
    """
97
    def compute(self, today, assets, out, base, weight):
98
        out[:] = nansum(base * weight, axis=0) / nansum(weight, axis=0)
99
100
101
class VWAP(WeightedAverageValue):
102
    """
103
    Volume Weighted Average Price
104
105
    **Default Inputs:** [USEquityPricing.close, USEquityPricing.volume]
106
107
    **Default Window Length:** None
108
    """
109
    inputs = (USEquityPricing.close, USEquityPricing.volume)
110
111
112
class MaxDrawdown(CustomFactor, SingleInputMixin):
113
    """
114
    Max Drawdown
115
116
    **Default Inputs:** None
117
118
    **Default Window Length:** None
119
    """
120
    ctx = ignore_nanwarnings()
121
122
    def compute(self, today, assets, out, data):
123
        drawdowns = fmax.accumulate(data, axis=0) - data
124
        drawdowns[isnan(drawdowns)] = NINF
125
        drawdown_ends = nanargmax(drawdowns, axis=0)
126
127
        # TODO: Accelerate this loop in Cython or Numba.
128
        for i, end in enumerate(drawdown_ends):
129
            peak = nanmax(data[:end + 1, i])
130
            out[i] = (peak - data[end, i]) / data[end, i]
131
132
133
def DollarVolume():
134
    """
135
    Returns a Factor computing the product of most recent close price and
136
    volume.
137
    """
138
    return USEquityPricing.close.latest * USEquityPricing.volume.latest
139
140
141
class _ExponentialWeightedFactor(SingleInputMixin, CustomFactor):
142
    """
143
    Base class for factors implementing exponential-weighted operations.
144
145
    **Default Inputs:** None
146
    **Default Window Length:** None
147
148
    Parameters
149
    ----------
150
    inputs : length-1 list or tuple of BoundColumn
151
        The expression over which to compute the average.
152
    window_length : int > 0
153
        Length of the lookback window over which to compute the average.
154
    decay_rate : float, 0 < decay_rate <= 1
155
        Weighting factor by which to discount past observations.
156
157
        When calculating historical averages, rows are multiplied by the
158
        sequence::
159
160
            decay_rate, decay_rate ** 2, decay_rate ** 3, ...
161
162
    Methods
163
    -------
164
    weights
165
    from_span
166
    from_halflife
167
    from_center_of_mass
168
    """
169
    params = ('decay_rate',)
170
171
    @staticmethod
172
    def weights(length, decay_rate):
173
        """
174
        Return weighting vector for an exponential moving statistic on `length`
175
        rows with a decay rate of `decay_rate`.
176
        """
177
        return full(length, decay_rate) ** arange(length + 1, 1, -1)
178
179
    @classmethod
180
    @expect_types(span=Number)
181
    def from_span(cls, inputs, window_length, span):
182
        """
183
        Convenience constructor for passing `decay_rate` in terms of `span`.
184
185
        Forwards `decay_rate` as `1 - (2.0 / (1 + span))`.  This provides the
186
        behavior equivalent to passing `span` to pandas.ewma.
187
        """
188
        if span <= 1:
189
            raise ValueError(
190
                "`span` must be a positive number. %s was passed." % span
191
            )
192
193
        decay_rate = (1.0 - (2.0 / (1.0 + span)))
194
        assert 0.0 < decay_rate <= 1.0
195
196
        return cls(
197
            inputs=inputs,
198
            window_length=window_length,
199
            decay_rate=decay_rate,
200
        )
201
202
    @classmethod
203
    @expect_types(halflife=Number)
204
    def from_halflife(cls, inputs, window_length, halflife):
205
        """
206
        Convenience constructor for passing `decay_rate` in terms of half life.
207
208
        Forwards `decay_rate` as `exp(log(.5) / halflife)`.  This provides
209
        the behavior equivalent to passing `halflife` to pandas.ewma.
210
        """
211
        if halflife <= 0:
212
            raise ValueError(
213
                "`span` must be a positive number. %s was passed." % halflife
214
            )
215
        decay_rate = exp(log(.5) / halflife)
216
        assert 0.0 < decay_rate <= 1.0
217
218
        return cls(
219
            inputs=inputs,
220
            window_length=window_length,
221
            decay_rate=decay_rate,
222
        )
223
224
    @classmethod
225
    def from_center_of_mass(cls, inputs, window_length, center_of_mass):
226
        """
227
        Convenience constructor for passing `decay_rate` in terms of center of
228
        mass.
229
230
        Forwards `decay_rate` as `1 - (1 / center_of_mass)`.  This provides
231
        behavior equivalent to passing `center_of_mass` to pandas.ewma.
232
        """
233
        return cls(
234
            inputs=inputs,
235
            window_length=window_length,
236
            decay_rate=(1.0 - (1.0 / (1.0 + center_of_mass))),
237
        )
238
239
240
class ExponentialWeightedMovingAverage(_ExponentialWeightedFactor):
241
    """
242
    Exponentially Weighted Moving Average
243
244
    **Default Inputs:** None
245
    **Default Window Length:** None
246
247
    Parameters
248
    ----------
249
    inputs : length-1 list/tuple of BoundColumn
250
        The expression over which to compute the average.
251
    window_length : int > 0
252
        Length of the lookback window over which to compute the average.
253
    decay_rate : float, 0 < decay_rate <= 1
254
        Weighting factor by which to discount past observations.
255
256
        When calculating historical averages, rows are multiplied by the
257
        sequence::
258
259
            decay_rate, decay_rate ** 2, decay_rate ** 3, ...
260
261
    See Also
262
    --------
263
    pandas.ewma
264
    """
265
    def compute(self, today, assets, out, data, decay_rate):
266
        out[:] = average(
267
            data,
268
            axis=0,
269
            weights=self.weights(len(data), decay_rate),
270
        )
271
272
273
class ExponentialWeightedStandardDeviation(_ExponentialWeightedFactor):
274
    """
275
    Exponentially Weighted Moving Standard Deviation
276
277
    **Default Inputs:** None
278
    **Default Window Length:** None
279
280
    Parameters
281
    ----------
282
    inputs : length-1 list/tuple of BoundColumn
283
        The expression over which to compute the average.
284
    window_length : int > 0
285
        Length of the lookback window over which to compute the average.
286
    decay_rate : float, 0 < decay_rate <= 1
287
        Weighting factor by which to discount past observations.
288
289
        When calculating historical averages, rows are multiplied by the
290
        sequence::
291
292
            decay_rate, decay_rate ** 2, decay_rate ** 3, ...
293
294
    See Also
295
    --------
296
    pandas.ewmstd
297
    """
298
299
    def compute(self, today, assets, out, data, decay_rate):
300
        weights = self.weights(len(data), decay_rate)
301
302
        mean = average(data, axis=0, weights=weights)
303
        variance = average((data - mean) ** 2, axis=0, weights=weights)
304
305
        squared_weight_sum = (np_sum(weights) ** 2)
306
        bias_correction = (
307
            squared_weight_sum / (squared_weight_sum - np_sum(weights ** 2))
308
        )
309
        out[:] = sqrt(variance * bias_correction)
310
311
312
# Convenience aliases.
313
EWMA = ExponentialWeightedMovingAverage
314
EWMSTD = ExponentialWeightedStandardDeviation
315