mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-07 01:06:52 +08:00
Merge pull request #1263 from quantopian/cloud-computing
Ichimoku Cloud
This commit is contained in:
@@ -13,7 +13,8 @@ from zipline.pipeline.term import AssetExists
|
||||
from zipline.pipeline.factors import (
|
||||
BollingerBands,
|
||||
Aroon,
|
||||
FastStochasticOscillator
|
||||
FastStochasticOscillator,
|
||||
IchimokuKinkoHyo,
|
||||
)
|
||||
from zipline.testing import ExplodingObject, parameter_space
|
||||
from zipline.testing.fixtures import WithAssetFinder, ZiplineTestCase
|
||||
@@ -237,3 +238,119 @@ class TestFastStochasticOscillator(WithTechnicalFactor, ZiplineTestCase):
|
||||
)
|
||||
|
||||
assert_equal(out, expected_out_k)
|
||||
|
||||
|
||||
class IchimokuKinkoHyoTestCase(ZiplineTestCase):
|
||||
def test_ichimoku_kinko_hyo(self):
|
||||
window_length = 52
|
||||
today = pd.Timestamp('2014', tz='utc')
|
||||
nassets = 5
|
||||
assets = pd.Index(np.arange(nassets))
|
||||
days_col = np.arange(window_length)[:, np.newaxis]
|
||||
highs = np.arange(nassets) + 2 + days_col
|
||||
closes = np.arange(nassets) + 1 + days_col
|
||||
lows = np.arange(nassets) + days_col
|
||||
|
||||
tenkan_sen_length = 9
|
||||
kijun_sen_length = 26
|
||||
chikou_span_length = 26
|
||||
ichimoku_kinko_hyo = IchimokuKinkoHyo(
|
||||
window_length=window_length,
|
||||
tenkan_sen_length=tenkan_sen_length,
|
||||
kijun_sen_length=kijun_sen_length,
|
||||
chikou_span_length=chikou_span_length,
|
||||
)
|
||||
|
||||
dtype = [
|
||||
('tenkan_sen', 'f8'),
|
||||
('kijun_sen', 'f8'),
|
||||
('senkou_span_a', 'f8'),
|
||||
('senkou_span_b', 'f8'),
|
||||
('chikou_span', 'f8'),
|
||||
]
|
||||
out = np.recarray(
|
||||
shape=(nassets,),
|
||||
dtype=dtype,
|
||||
buf=np.empty(shape=(nassets,), dtype=dtype),
|
||||
)
|
||||
ichimoku_kinko_hyo.compute(
|
||||
today,
|
||||
assets,
|
||||
out,
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
tenkan_sen_length,
|
||||
kijun_sen_length,
|
||||
chikou_span_length,
|
||||
)
|
||||
|
||||
expected_tenkan_sen = np.array([
|
||||
(53 + 43) / 2,
|
||||
(54 + 44) / 2,
|
||||
(55 + 45) / 2,
|
||||
(56 + 46) / 2,
|
||||
(57 + 47) / 2,
|
||||
])
|
||||
expected_kijun_sen = np.array([
|
||||
(53 + 26) / 2,
|
||||
(54 + 27) / 2,
|
||||
(55 + 28) / 2,
|
||||
(56 + 29) / 2,
|
||||
(57 + 30) / 2,
|
||||
])
|
||||
expected_senkou_span_a = (expected_tenkan_sen + expected_kijun_sen) / 2
|
||||
expected_senkou_span_b = np.array([
|
||||
(53 + 0) / 2,
|
||||
(54 + 1) / 2,
|
||||
(55 + 2) / 2,
|
||||
(56 + 3) / 2,
|
||||
(57 + 4) / 2,
|
||||
])
|
||||
expected_chikou_span = np.array([
|
||||
27.0,
|
||||
28.0,
|
||||
29.0,
|
||||
30.0,
|
||||
31.0,
|
||||
])
|
||||
|
||||
assert_equal(
|
||||
out.tenkan_sen,
|
||||
expected_tenkan_sen,
|
||||
msg='tenkan_sen',
|
||||
)
|
||||
assert_equal(
|
||||
out.kijun_sen,
|
||||
expected_kijun_sen,
|
||||
msg='kijun_sen',
|
||||
)
|
||||
assert_equal(
|
||||
out.senkou_span_a,
|
||||
expected_senkou_span_a,
|
||||
msg='senkou_span_a',
|
||||
)
|
||||
assert_equal(
|
||||
out.senkou_span_b,
|
||||
expected_senkou_span_b,
|
||||
msg='senkou_span_b',
|
||||
)
|
||||
assert_equal(
|
||||
out.chikou_span,
|
||||
expected_chikou_span,
|
||||
msg='chikou_span',
|
||||
)
|
||||
|
||||
@parameter_space(
|
||||
arg={'tenkan_sen_length', 'kijun_sen_length', 'chikou_span_length'},
|
||||
)
|
||||
def test_input_validation(self, arg):
|
||||
window_length = 52
|
||||
|
||||
with self.assertRaises(ValueError) as e:
|
||||
IchimokuKinkoHyo(**{arg: window_length + 1})
|
||||
|
||||
assert_equal(
|
||||
str(e.exception),
|
||||
'%s must be <= the window_length: 53 > 52' % arg,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,8 @@ from collections import Counter
|
||||
from itertools import product
|
||||
from unittest import TestCase
|
||||
|
||||
from toolz import assoc
|
||||
|
||||
from zipline.assets import Asset
|
||||
from zipline.errors import (
|
||||
DTypeNotSpecified,
|
||||
@@ -31,7 +33,12 @@ from zipline.pipeline.factors import RecarrayField
|
||||
from zipline.pipeline.sentinels import NotSpecified
|
||||
from zipline.pipeline.term import AssetExists, Slice
|
||||
from zipline.testing import parameter_space
|
||||
from zipline.testing.predicates import assert_equal, assert_raises
|
||||
from zipline.testing.predicates import (
|
||||
assert_equal,
|
||||
assert_raises,
|
||||
assert_raises_regex,
|
||||
assert_regex,
|
||||
)
|
||||
from zipline.utils.numpy_utils import (
|
||||
bool_dtype,
|
||||
categorical_dtype,
|
||||
@@ -433,10 +440,50 @@ class ObjectIdentityTestCase(TestCase):
|
||||
|
||||
with assert_raises(TypeError) as e:
|
||||
self.SomeFactorParameterized(a=[], b=[])
|
||||
assert_equal(
|
||||
assert_regex(
|
||||
str(e.exception),
|
||||
"SomeFactorParameterized expected a hashable value for parameter"
|
||||
" 'a', but got [] instead.",
|
||||
r"SomeFactorParameterized expected a hashable value for parameter"
|
||||
r" '(a|b)', but got \[\] instead\.",
|
||||
)
|
||||
|
||||
def test_parameterized_term_default_value(self):
|
||||
defaults = {'a': 'default for a', 'b': 'default for b'}
|
||||
|
||||
class F(Factor):
|
||||
params = defaults
|
||||
|
||||
inputs = (SomeDataSet.foo,)
|
||||
dtype = 'f8'
|
||||
window_length = 5
|
||||
|
||||
assert_equal(F().params, defaults)
|
||||
assert_equal(F(a='new a').params, assoc(defaults, 'a', 'new a'))
|
||||
assert_equal(F(b='new b').params, assoc(defaults, 'b', 'new b'))
|
||||
assert_equal(
|
||||
F(a='new a', b='new b').params,
|
||||
{'a': 'new a', 'b': 'new b'},
|
||||
)
|
||||
|
||||
def test_parameterized_term_default_value_with_not_specified(self):
|
||||
defaults = {'a': 'default for a', 'b': NotSpecified}
|
||||
|
||||
class F(Factor):
|
||||
params = defaults
|
||||
|
||||
inputs = (SomeDataSet.foo,)
|
||||
dtype = 'f8'
|
||||
window_length = 5
|
||||
|
||||
pattern = r"F expected a keyword parameter 'b'\."
|
||||
with assert_raises_regex(TypeError, pattern):
|
||||
F()
|
||||
with assert_raises_regex(TypeError, pattern):
|
||||
F(a='new a')
|
||||
|
||||
assert_equal(F(b='new b').params, assoc(defaults, 'b', 'new b'))
|
||||
assert_equal(
|
||||
F(a='new a', b='new b').params,
|
||||
{'a': 'new a', 'b': 'new b'},
|
||||
)
|
||||
|
||||
def test_bad_input(self):
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import re
|
||||
|
||||
|
||||
_symbol_delimiter_regex = re.compile(r'[./\-_]')
|
||||
|
||||
|
||||
def split_nasdaq(symbol):
|
||||
sym = re.replace(_symbol_delimiter_regex, '', symbol)
|
||||
return sym[:4], sym[4:]
|
||||
|
||||
|
||||
def split_nyse(symbol):
|
||||
return re.split(_symbol_delimiter_regex, symbol, maxsplit=1)
|
||||
@@ -22,6 +22,7 @@ from .technical import (
|
||||
ExponentialWeightedMovingAverage,
|
||||
ExponentialWeightedMovingStdDev,
|
||||
FastStochasticOscillator,
|
||||
IchimokuKinkoHyo,
|
||||
MaxDrawdown,
|
||||
Returns,
|
||||
RSI,
|
||||
@@ -43,6 +44,7 @@ __all__ = [
|
||||
'ExponentialWeightedMovingStdDev',
|
||||
'Factor',
|
||||
'FastStochasticOscillator',
|
||||
'IchimokuKinkoHyo',
|
||||
'Latest',
|
||||
'MaxDrawdown',
|
||||
'RecarrayField',
|
||||
|
||||
@@ -445,8 +445,7 @@ class BollingerBands(CustomFactor):
|
||||
class Aroon(CustomFactor):
|
||||
"""
|
||||
Aroon technical indicator.
|
||||
https://www.fidelity.com/learning-center/trading-investing/technical
|
||||
-analysis/technical-indicator-guide/aroon-indicator
|
||||
https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/aroon-indicator # noqa
|
||||
|
||||
**Defaults Inputs:** USEquityPricing.low, USEquityPricing.high
|
||||
|
||||
@@ -521,3 +520,73 @@ class FastStochasticOscillator(CustomFactor):
|
||||
global_dict={},
|
||||
out=out,
|
||||
)
|
||||
|
||||
|
||||
class IchimokuKinkoHyo(CustomFactor):
|
||||
"""Compute the various metrics for the Ichimoku Kinko Hyo (Ichimoku Cloud).
|
||||
http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:ichimoku_cloud # noqa
|
||||
|
||||
**Default Inputs:** :data:`zipline.pipeline.data.USEquityPricing.high`
|
||||
:data:`zipline.pipeline.data.USEquityPricing.low`
|
||||
:data:`zipline.pipeline.data.USEquityPricing.close`
|
||||
**Default Window Length:** 52
|
||||
|
||||
Parameters
|
||||
----------
|
||||
window_length : int > 0
|
||||
The length the the window for the senkou span b.
|
||||
tenkan_sen_length : int >= 0, <= window_length
|
||||
The length of the window for the tenkan-sen.
|
||||
kijun_sen_length : int >= 0, <= window_length
|
||||
The length of the window for the kijou-sen.
|
||||
chikou_span_length : int >= 0, <= window_length
|
||||
The lag for the chikou span.
|
||||
"""
|
||||
|
||||
params = {
|
||||
'tenkan_sen_length': 9,
|
||||
'kijun_sen_length': 26,
|
||||
'chikou_span_length': 26,
|
||||
}
|
||||
inputs = USEquityPricing.high, USEquityPricing.close
|
||||
outputs = (
|
||||
'tenkan_sen',
|
||||
'kijun_sen',
|
||||
'senkou_span_a',
|
||||
'senkou_span_b',
|
||||
'chikou_span',
|
||||
)
|
||||
window_length = 52
|
||||
|
||||
def _validate(self):
|
||||
super(IchimokuKinkoHyo, self)._validate()
|
||||
for k, v in self.params.items():
|
||||
if v > self.window_length:
|
||||
raise ValueError(
|
||||
'%s must be <= the window_length: %s > %s' % (
|
||||
k, v, self.window_length,
|
||||
),
|
||||
)
|
||||
|
||||
def compute(self,
|
||||
today,
|
||||
assets,
|
||||
out,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
tenkan_sen_length,
|
||||
kijun_sen_length,
|
||||
chikou_span_length):
|
||||
|
||||
out.tenkan_sen = tenkan_sen = (
|
||||
high[-tenkan_sen_length:].max(axis=0) +
|
||||
low[-tenkan_sen_length:].min(axis=0)
|
||||
) / 2
|
||||
out.kijun_sen = kijun_sen = (
|
||||
high[-kijun_sen_length:].max(axis=0) +
|
||||
low[-kijun_sen_length:].min(axis=0)
|
||||
) / 2
|
||||
out.senkou_span_a = (tenkan_sen + kijun_sen) / 2
|
||||
out.senkou_span_b = (high.max(axis=0) + low.min(axis=0)) / 2
|
||||
out.chikou_span = close[chikou_span_length]
|
||||
|
||||
@@ -3,6 +3,7 @@ Base class for Filters, Factors and Classifiers
|
||||
"""
|
||||
from abc import ABCMeta, abstractproperty
|
||||
from bisect import insort
|
||||
from collections import Mapping
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from numpy import (
|
||||
@@ -146,10 +147,16 @@ class Term(with_metaclass(ABCMeta, object)):
|
||||
TypeError
|
||||
Raised if any parameter values are not passed or not hashable.
|
||||
"""
|
||||
params = cls.params
|
||||
if not isinstance(params, Mapping):
|
||||
params = {k: NotSpecified for k in params}
|
||||
param_values = []
|
||||
for key in cls.params:
|
||||
for key, default_value in params.items():
|
||||
try:
|
||||
value = kwargs.pop(key)
|
||||
value = kwargs.pop(key, default_value)
|
||||
if value is NotSpecified:
|
||||
raise KeyError(key)
|
||||
|
||||
# Check here that the value is hashable so that we fail here
|
||||
# instead of trying to hash the param values tuple later.
|
||||
hash(value)
|
||||
@@ -171,8 +178,8 @@ class Term(with_metaclass(ABCMeta, object)):
|
||||
)
|
||||
)
|
||||
|
||||
param_values.append(value)
|
||||
return tuple(zip(cls.params, param_values))
|
||||
param_values.append((key, value))
|
||||
return tuple(param_values)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from contextlib import contextmanager
|
||||
import datetime
|
||||
from functools import partial
|
||||
import inspect
|
||||
import re
|
||||
|
||||
from nose.tools import ( # noqa
|
||||
assert_almost_equal,
|
||||
@@ -205,6 +207,47 @@ def assert_is_subclass(subcls, cls, msg=''):
|
||||
)
|
||||
|
||||
|
||||
def assert_regex(result, expected, msg=''):
|
||||
"""Assert that ``expected`` matches the result.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
result : str
|
||||
The string to search.
|
||||
expected : str or compiled regex
|
||||
The pattern to search for in ``result``.
|
||||
msg : str, optional
|
||||
An extra assertion message to print if this fails.
|
||||
"""
|
||||
assert re.search(expected, result), (
|
||||
'%s%r not found in %r' % (_fmt_msg(msg), expected, result)
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def assert_raises_regex(exc, pattern, msg=''):
|
||||
"""Assert that some exception is raised in a context and that the message
|
||||
matches some pattern.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exc : type or tuple[type]
|
||||
The exception type or types to expect.
|
||||
pattern : str or compiled regex
|
||||
The pattern to search for in the str of the raised exception.
|
||||
msg : str, optional
|
||||
An extra assertion message to print if this fails.
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
except exc as e:
|
||||
assert re.search(pattern, str(e)), (
|
||||
'%s%r not found in %r' % (_fmt_msg(msg), pattern, str(e))
|
||||
)
|
||||
else:
|
||||
raise AssertionError('%s%s was not raised' % (_fmt_msg(msg), exc))
|
||||
|
||||
|
||||
@dispatch(object, object)
|
||||
def assert_equal(result, expected, path=(), msg='', **kwargs):
|
||||
"""Assert that two objects are equal using the ``==`` operator.
|
||||
|
||||
Reference in New Issue
Block a user