Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
that bar's value, ``handle.peek(bar)`` evaluates a forming bar without
committing it, and ``handle.copy()`` forks it. ``stream.SMA.open_and_fill()``
returns the handle and the Function API's series in one pass. A multi-output
function answers with a named tuple. The old last-value functions --
``talib.stream.SMA``, ``talib.stream_SMA``, and their ``_ta_lib.pyi`` stubs --
are gone; ``talib/stream.pyi`` types the handles instead.
function answers with the same tuple the Function API returns. The old
last-value functions -- ``talib.stream.SMA``, ``talib.stream_SMA``, and their
``_ta_lib.pyi`` stubs -- are gone; ``talib/stream.pyi`` types the handles
instead.

Migrating is ``stream.X(...)`` -> ``stream.X(...).value``, and the compiler
cannot find the sites for you: ``if stream.CDLDOJI(o, h, l, c):`` used to test
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,13 +557,14 @@ fork = s.copy() # an independent handle at the same bar

`stream.SMA` takes exactly the arguments `talib.SMA` takes. A single-output
function answers with a `float` (an `int` where the Function API returns an
integer array); a multi-output one with a named tuple that still unpacks like
the Function API's tuple:
integer array); a multi-output one with the same tuple the Function API returns:

```python
m = stream.MACD(close)
macd, macdsignal, macdhist = m.update(price)
m.value.macdhist
m.value[2] # the histogram, last bar

abstract.Function('MACD').output_names # ['macd', 'macdsignal', 'macdhist']
```

Opening needs at least `lookback + 1` bars, which `abstract` knows, and a little
Expand Down
274 changes: 114 additions & 160 deletions talib/_stream.pxi

Large diffs are not rendered by default.

53,353 changes: 25,462 additions & 27,891 deletions talib/_ta_lib.c

Large diffs are not rendered by default.

12 changes: 3 additions & 9 deletions talib/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@

s, rsi = stream.RSI.open_and_fill(history, timeperiod=14)

A multi-output function answers with a named tuple whose fields are the output
names in the function's docstring; a single-output one with a bare float (or
int). Handles cannot be pickled.
A multi-output function answers with a plain tuple, in the order the Function
API returns and named by ``abstract.Function(name).output_names``; a
single-output one with a bare float (or int). Handles cannot be pickled.
"""
import talib._ta_lib as _ta_lib
from talib._ta_lib import OutRange, Stream, __TA_FUNCTION_NAMES__
Expand All @@ -57,9 +57,3 @@
for func_name in __TA_FUNCTION_NAMES__:
globals()[func_name] = getattr(_ta_lib, '%s_Stream' % func_name)
__all__.append(func_name)
# the named tuple a multi-output handle answers with
value_name = '%s_Value' % func_name
value_type = getattr(_ta_lib, value_name, None)
if value_type is not None:
globals()[value_name] = value_type
__all__.append(value_name)
240 changes: 69 additions & 171 deletions talib/stream.pyi

Large diffs are not rendered by default.

27 changes: 21 additions & 6 deletions tests/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,14 @@ def test_open_and_fill_matches_batch(name, datasets):
assert repr(got) == repr(want[-1].item())
assert handle.out_range == (lookback(name), len(data['close']) - lookback(name))
if len(expected) > 1:
assert handle.value._fields == tuple(abstract.Function(name).output_names)
# a multi-output handle answers with a plain tuple, the type and arity
# the batch tier already returns; the loops above pin the order
assert type(handle.value) is tuple
assert len(handle.value) == len(expected)
assert type(filled) is tuple
assert len(filled) == len(expected)
else:
assert not isinstance(handle.value, tuple)


@pytest.mark.parametrize('name', FUNCTIONS)
Expand Down Expand Up @@ -286,11 +293,19 @@ def test_the_corpus_is_what_the_library_says_streams():
assert all(isinstance(getattr(stream, name), type) for name in FUNCTIONS)


def test_multi_output_is_a_named_tuple(datasets):
handle = stream.MACD(datasets[0]['close'])
macd, macdsignal, macdhist = handle.value
assert (handle.value.macd, handle.value.macdsignal, handle.value.macdhist) \
== (macd, macdsignal, macdhist)
def test_multi_output_is_a_plain_tuple(datasets):
"""A handle answers with the tuple the Function API returns: same type, same
arity, same order. Order is the whole contract of a positional result, so
pin it against the batch tier rather than against a field name."""
close = datasets[0]['close']
value = stream.MACD(close).value
assert type(value) is tuple
macd, macdsignal, macdhist = value
batch = talib.MACD(close)
assert type(batch) is tuple
assert len(value) == len(batch)
for got, want in zip(value, batch):
assert repr(got) == repr(want[-1].item())


def test_single_output_is_a_scalar(datasets):
Expand Down
16 changes: 5 additions & 11 deletions tools/generate_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,8 @@ def emit(func, docstring):
# An index output counts from the first bar the stream opened on; the batch
# tier reports it in the caller's coordinates, so shift it back the same way.
shift = ' + self._begidx' if 'INDEX' in name else ''
value = (outputs[0][1] + shift if len(outputs) == 1 else '%s_Value(%s)' % (
name, ', '.join(py + shift for _, py in outputs)))
value = (outputs[0][1] + shift if len(outputs) == 1
else '(%s)' % ', '.join(py + shift for _, py in outputs))
live = '<%s*>self._handle' % handle
lookback_args = ', '.join(py for _, py, _ in params)
out = []
Expand Down Expand Up @@ -294,9 +294,6 @@ def opened(*tail):
return (['&handle'] + ['<double*>a_%s.data + begidx' % py for py in inputs]
+ ['historylen'] + [py for _, py, _ in params] + list(tail))

if len(outputs) > 1:
out.append('%s_Value = namedtuple("%s_Value", "%s", module=__name__)\n'
% (name, name, ' '.join(py[3:] for _, py in outputs)))
out.append('cdef class %s(Stream):' % cls)
out.append(' """%s"""' % docstring)
out.append('')
Expand Down Expand Up @@ -345,8 +342,7 @@ def opened(*tail):
out.append(' stream._begidx = begidx')
filled = ['_stream_like((%s,), %s)' % (', '.join(inputs), py) for _, py in outputs]
out.append(' return stream, %s' % (
filled[0] if len(outputs) == 1
else '%s_Value(%s)' % (name, ', '.join(filled))))
filled[0] if len(outputs) == 1 else '(%s)' % ', '.join(filled)))
for verb in ('Update', 'Peek'):
out.append('')
out.append(' @cython.binding(False)')
Expand Down Expand Up @@ -412,10 +408,8 @@ def emit_stub(func, documented):
for ctype, _ in outputs]
out = []
if len(outputs) > 1:
out.append('class %s_Value(NamedTuple):' % name)
out.extend(' %s: %s' % (py[3:], t) for (_, py), t in zip(outputs, scalars))
out.append('')
value, filled = '%s_Value' % name, 'Tuple[%s]' % ', '.join(arrays)
value = 'Tuple[%s]' % ', '.join(scalars)
filled = 'Tuple[%s]' % ', '.join(arrays)
else:
value, filled = scalars[0], arrays[0]
bars = ', '.join('%s: float' % py for py in inputs)
Expand Down
Loading