td-sequential 1.0.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.
- td_sequential/__init__.py +6 -0
- td_sequential/core.py +260 -0
- td_sequential/pandas.py +50 -0
- td_sequential/py.typed +0 -0
- td_sequential-1.0.0.dist-info/METADATA +145 -0
- td_sequential-1.0.0.dist-info/RECORD +8 -0
- td_sequential-1.0.0.dist-info/WHEEL +4 -0
- td_sequential-1.0.0.dist-info/licenses/LICENSE +21 -0
td_sequential/core.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""TD Sequential setup, countdown and support/resistance counter.
|
|
2
|
+
|
|
3
|
+
Pure Python, no dependencies. Written from SPEC.md: one record per closed bar,
|
|
4
|
+
oldest first. Nothing here is trading advice; a count describes past prices.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Iterable, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
__all__ = ["TdBar", "CountdownState", "Latest", "compute", "latest"]
|
|
14
|
+
|
|
15
|
+
MAX_LEVELS = 20
|
|
16
|
+
IDLE = 14 # cancellation / "no countdown in progress" value
|
|
17
|
+
|
|
18
|
+
# Attribute / key names accepted for each bar field, first match wins.
|
|
19
|
+
_NAMES = {
|
|
20
|
+
"t": ("t", "time", "timestamp"),
|
|
21
|
+
"o": ("o", "open"),
|
|
22
|
+
"h": ("h", "high"),
|
|
23
|
+
"l": ("l", "low"),
|
|
24
|
+
"c": ("c", "close"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class TdBar:
|
|
30
|
+
"""One output record per input bar (SPEC.md, section 4)."""
|
|
31
|
+
|
|
32
|
+
t: Any
|
|
33
|
+
buy_setup: int
|
|
34
|
+
sell_setup: int
|
|
35
|
+
buy_countdown: int
|
|
36
|
+
sell_countdown: int
|
|
37
|
+
resistance: Optional[float]
|
|
38
|
+
support: Optional[float]
|
|
39
|
+
buy_deferred: bool
|
|
40
|
+
sell_deferred: bool
|
|
41
|
+
buy_perfected: bool
|
|
42
|
+
sell_perfected: bool
|
|
43
|
+
bullish_flip: bool
|
|
44
|
+
bearish_flip: bool
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class CountdownState:
|
|
49
|
+
"""A countdown in progress: `count` of 13, `paused` when the last bar did not qualify."""
|
|
50
|
+
|
|
51
|
+
count: int
|
|
52
|
+
paused: bool
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Latest:
|
|
57
|
+
"""Summary of the final bar of a `compute()` result."""
|
|
58
|
+
|
|
59
|
+
t: Any
|
|
60
|
+
buy_setup: int
|
|
61
|
+
sell_setup: int
|
|
62
|
+
buy_countdown: Optional[CountdownState]
|
|
63
|
+
sell_countdown: Optional[CountdownState]
|
|
64
|
+
buy_completed_13_now: bool
|
|
65
|
+
sell_completed_13_now: bool
|
|
66
|
+
last_buy_setup_9: Any
|
|
67
|
+
last_sell_setup_9: Any
|
|
68
|
+
last_buy_countdown_13: Any
|
|
69
|
+
last_sell_countdown_13: Any
|
|
70
|
+
resistance: Optional[float]
|
|
71
|
+
support: Optional[float]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _field(bar: Any, key: str, index: int) -> Any:
|
|
75
|
+
names = _NAMES[key]
|
|
76
|
+
if isinstance(bar, dict):
|
|
77
|
+
for name in names:
|
|
78
|
+
if name in bar:
|
|
79
|
+
return bar[name]
|
|
80
|
+
else:
|
|
81
|
+
for name in names:
|
|
82
|
+
if hasattr(bar, name):
|
|
83
|
+
return getattr(bar, name)
|
|
84
|
+
raise ValueError(f"bar {index}: missing field '{names[0]}' (also accepted: {', '.join(names[1:]) or 'none'})")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _price(value: Any, key: str, index: int) -> float:
|
|
88
|
+
if isinstance(value, bool):
|
|
89
|
+
raise TypeError(f"bar {index}: '{key}' must be a number, got bool")
|
|
90
|
+
try:
|
|
91
|
+
number = float(value)
|
|
92
|
+
except (TypeError, ValueError):
|
|
93
|
+
raise TypeError(f"bar {index}: '{key}' must be a number, got {type(value).__name__}") from None
|
|
94
|
+
if math.isnan(number):
|
|
95
|
+
raise ValueError(f"bar {index}: '{key}' is NaN")
|
|
96
|
+
return number
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _normalize(bars: Iterable[Any]) -> List[Tuple[Any, float, float, float]]:
|
|
100
|
+
"""Return (t, high, low, close) per bar. Accepts dicts, (t,o,h,l,c) tuples or objects."""
|
|
101
|
+
rows: List[Tuple[Any, float, float, float]] = []
|
|
102
|
+
for i, bar in enumerate(bars):
|
|
103
|
+
if isinstance(bar, (tuple, list)):
|
|
104
|
+
if len(bar) != 5:
|
|
105
|
+
raise ValueError(f"bar {i}: a tuple bar must be (t, o, h, l, c), got {len(bar)} items")
|
|
106
|
+
t, o, h, l, c = bar
|
|
107
|
+
else:
|
|
108
|
+
t, o, h, l, c = (_field(bar, k, i) for k in ("t", "o", "h", "l", "c"))
|
|
109
|
+
_price(o, "o", i)
|
|
110
|
+
rows.append((t, _price(h, "h", i), _price(l, "l", i), _price(c, "c", i)))
|
|
111
|
+
return rows
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _Countdown:
|
|
115
|
+
"""State of one side's countdown (SPEC.md, section 3)."""
|
|
116
|
+
|
|
117
|
+
def __init__(self) -> None:
|
|
118
|
+
self.value = 0
|
|
119
|
+
self.eight_close = math.nan
|
|
120
|
+
|
|
121
|
+
def step(self, own9: bool, other9: bool, level_active: bool, qualifies: bool, test: bool, close: float) -> Tuple[int, bool]:
|
|
122
|
+
prev = self.value
|
|
123
|
+
deferred = False
|
|
124
|
+
if own9:
|
|
125
|
+
value = 1 if qualifies else 0
|
|
126
|
+
elif other9 or not level_active:
|
|
127
|
+
value = IDLE
|
|
128
|
+
elif abs(prev) == 12 and qualifies and not test:
|
|
129
|
+
value, deferred = -12, True
|
|
130
|
+
elif qualifies:
|
|
131
|
+
value = abs(prev) + 1
|
|
132
|
+
else:
|
|
133
|
+
value = 0 if prev == 0 else -abs(prev)
|
|
134
|
+
if value == 8: # only a transition to +8 gets here
|
|
135
|
+
self.eight_close = close
|
|
136
|
+
self.value = value
|
|
137
|
+
return value, deferred
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class _Level:
|
|
141
|
+
__slots__ = ("price", "active")
|
|
142
|
+
|
|
143
|
+
def __init__(self, price: float) -> None:
|
|
144
|
+
self.price = price
|
|
145
|
+
self.active = True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _add_level(levels: List[_Level], price: float) -> None:
|
|
149
|
+
if not price > 0 or any(x.active and x.price == price for x in levels):
|
|
150
|
+
return
|
|
151
|
+
levels.append(_Level(price))
|
|
152
|
+
if len(levels) > MAX_LEVELS:
|
|
153
|
+
levels.pop(0)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _reported(levels: List[_Level]) -> Optional[float]:
|
|
157
|
+
if levels and levels[-1].active:
|
|
158
|
+
return levels[-1].price
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def compute(bars: Iterable[Any]) -> List[TdBar]:
|
|
163
|
+
"""Compute one `TdBar` per input bar.
|
|
164
|
+
|
|
165
|
+
`bars` is ordered oldest first and holds closed bars only. Each bar is a dict
|
|
166
|
+
with keys t, o, h, l, c (long names such as "close" also work), a
|
|
167
|
+
(t, o, h, l, c) tuple, or an object with those attributes.
|
|
168
|
+
"""
|
|
169
|
+
rows = _normalize(bars)
|
|
170
|
+
out: List[TdBar] = []
|
|
171
|
+
resistances: List[_Level] = []
|
|
172
|
+
supports: List[_Level] = []
|
|
173
|
+
buy = _Countdown()
|
|
174
|
+
sell = _Countdown()
|
|
175
|
+
buy_setup = 0
|
|
176
|
+
sell_setup = 0
|
|
177
|
+
|
|
178
|
+
for i, (t, h, l, c) in enumerate(rows):
|
|
179
|
+
buy_setup = buy_setup + 1 if i >= 4 and c < rows[i - 4][3] else 0
|
|
180
|
+
sell_setup = sell_setup + 1 if i >= 4 and c > rows[i - 4][3] else 0
|
|
181
|
+
buy9 = buy_setup == 9
|
|
182
|
+
sell9 = sell_setup == 9
|
|
183
|
+
|
|
184
|
+
if buy9:
|
|
185
|
+
_add_level(resistances, max(r[1] for r in rows[i - 8 : i + 1]))
|
|
186
|
+
if sell9:
|
|
187
|
+
_add_level(supports, min(r[2] for r in rows[i - 8 : i + 1]))
|
|
188
|
+
for r in resistances:
|
|
189
|
+
if r.active and (c > r.price or l > r.price):
|
|
190
|
+
r.active = False
|
|
191
|
+
for s in supports:
|
|
192
|
+
if s.active and (c < s.price or h < s.price):
|
|
193
|
+
s.active = False
|
|
194
|
+
resistance = _reported(resistances)
|
|
195
|
+
support = _reported(supports)
|
|
196
|
+
|
|
197
|
+
l2 = rows[i - 2][2] if i >= 2 else math.nan
|
|
198
|
+
h2 = rows[i - 2][1] if i >= 2 else math.nan
|
|
199
|
+
b_value, b_deferred = buy.step(buy9, sell9, resistance is not None, i >= 2 and c <= l2, l <= buy.eight_close, c)
|
|
200
|
+
s_value, s_deferred = sell.step(sell9, buy9, support is not None, i >= 2 and c >= h2, h >= sell.eight_close, c)
|
|
201
|
+
|
|
202
|
+
# Information only: perfection on the setup-9 bar, and price flips.
|
|
203
|
+
buy_perfected = buy9 and min(rows[i - 1][2], l) < min(rows[i - 3][2], rows[i - 2][2])
|
|
204
|
+
sell_perfected = sell9 and max(rows[i - 1][1], h) > max(rows[i - 3][1], rows[i - 2][1])
|
|
205
|
+
bullish_flip = i >= 5 and rows[i - 1][3] < rows[i - 5][3] and c > rows[i - 4][3]
|
|
206
|
+
bearish_flip = i >= 5 and rows[i - 1][3] > rows[i - 5][3] and c < rows[i - 4][3]
|
|
207
|
+
|
|
208
|
+
out.append(
|
|
209
|
+
TdBar(
|
|
210
|
+
t=t,
|
|
211
|
+
buy_setup=buy_setup,
|
|
212
|
+
sell_setup=sell_setup,
|
|
213
|
+
buy_countdown=b_value,
|
|
214
|
+
sell_countdown=s_value,
|
|
215
|
+
resistance=resistance,
|
|
216
|
+
support=support,
|
|
217
|
+
buy_deferred=b_deferred,
|
|
218
|
+
sell_deferred=s_deferred,
|
|
219
|
+
buy_perfected=buy_perfected,
|
|
220
|
+
sell_perfected=sell_perfected,
|
|
221
|
+
bullish_flip=bullish_flip,
|
|
222
|
+
bearish_flip=bearish_flip,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _last_where(result: List[TdBar], predicate: Any) -> Any:
|
|
229
|
+
for record in reversed(result):
|
|
230
|
+
if predicate(record):
|
|
231
|
+
return record.t
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _countdown_state(value: int) -> Optional[CountdownState]:
|
|
236
|
+
if value != 0 and abs(value) < 13:
|
|
237
|
+
return CountdownState(count=abs(value), paused=value < 0)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def latest(result: List[TdBar]) -> Optional[Latest]:
|
|
242
|
+
"""Summary of the final bar of a `compute()` result, or None for an empty result."""
|
|
243
|
+
if not result:
|
|
244
|
+
return None
|
|
245
|
+
last = result[-1]
|
|
246
|
+
return Latest(
|
|
247
|
+
t=last.t,
|
|
248
|
+
buy_setup=last.buy_setup,
|
|
249
|
+
sell_setup=last.sell_setup,
|
|
250
|
+
buy_countdown=_countdown_state(last.buy_countdown),
|
|
251
|
+
sell_countdown=_countdown_state(last.sell_countdown),
|
|
252
|
+
buy_completed_13_now=last.buy_countdown == 13,
|
|
253
|
+
sell_completed_13_now=last.sell_countdown == 13,
|
|
254
|
+
last_buy_setup_9=_last_where(result, lambda r: r.buy_setup == 9),
|
|
255
|
+
last_sell_setup_9=_last_where(result, lambda r: r.sell_setup == 9),
|
|
256
|
+
last_buy_countdown_13=_last_where(result, lambda r: r.buy_countdown == 13),
|
|
257
|
+
last_sell_countdown_13=_last_where(result, lambda r: r.sell_countdown == 13),
|
|
258
|
+
resistance=last.resistance,
|
|
259
|
+
support=last.support,
|
|
260
|
+
)
|
td_sequential/pandas.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Optional pandas helper. Install with `pip install td-sequential[pandas]`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import fields
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .core import TdBar, compute
|
|
9
|
+
|
|
10
|
+
__all__ = ["add_td_sequential"]
|
|
11
|
+
|
|
12
|
+
_COLUMNS = [f.name for f in fields(TdBar) if f.name != "t"]
|
|
13
|
+
_REQUIRED = ("open", "high", "low", "close")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_td_sequential(df: Any, prefix: str = "td_") -> Any:
|
|
17
|
+
"""Return a copy of `df` with the TD Sequential columns added.
|
|
18
|
+
|
|
19
|
+
`df` needs `open`, `high`, `low` and `close` columns (case-insensitive), one
|
|
20
|
+
closed bar per row, oldest first; the index is used as the bar time. Added
|
|
21
|
+
columns are `<prefix>buy_setup`, `<prefix>sell_setup`, `<prefix>buy_countdown`,
|
|
22
|
+
`<prefix>sell_countdown`, `<prefix>resistance`, `<prefix>support` (NaN when
|
|
23
|
+
there is no active level), `<prefix>buy_deferred`, `<prefix>sell_deferred`,
|
|
24
|
+
`<prefix>buy_perfected`, `<prefix>sell_perfected`, `<prefix>bullish_flip`
|
|
25
|
+
and `<prefix>bearish_flip`.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
import pandas as pd
|
|
29
|
+
except ImportError as exc: # pragma: no cover - depends on the environment
|
|
30
|
+
raise ImportError("add_td_sequential needs pandas: pip install td-sequential[pandas]") from exc
|
|
31
|
+
|
|
32
|
+
lookup = {str(name).lower(): name for name in df.columns}
|
|
33
|
+
missing = [name for name in _REQUIRED if name not in lookup]
|
|
34
|
+
if missing:
|
|
35
|
+
raise KeyError(f"DataFrame is missing column(s): {', '.join(missing)}")
|
|
36
|
+
|
|
37
|
+
bars = (
|
|
38
|
+
(idx, o, h, l, c)
|
|
39
|
+
for idx, o, h, l, c in zip(df.index, *(df[lookup[name]] for name in _REQUIRED))
|
|
40
|
+
)
|
|
41
|
+
records = compute(bars)
|
|
42
|
+
|
|
43
|
+
out = df.copy()
|
|
44
|
+
for name in _COLUMNS:
|
|
45
|
+
values = [getattr(r, name) for r in records]
|
|
46
|
+
if name in ("resistance", "support"):
|
|
47
|
+
out[prefix + name] = pd.Series(values, index=df.index, dtype="float64")
|
|
48
|
+
else:
|
|
49
|
+
out[prefix + name] = pd.Series(values, index=df.index)
|
|
50
|
+
return out
|
td_sequential/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: td-sequential
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: TD Sequential indicator for Python: TD9 setup, countdown 13 and TDST levels on OHLC candles. Tested, zero dependencies.
|
|
5
|
+
Project-URL: Homepage, https://moolish.com/tools/td-sequential-counter/
|
|
6
|
+
Project-URL: Documentation, https://moolish.com/learn/td-sequential/
|
|
7
|
+
Project-URL: Source, https://github.com/moolish-com/td-sequential-python
|
|
8
|
+
Project-URL: Issues, https://github.com/moolish-com/td-sequential-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/moolish-com/td-sequential-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Moolish <support@moolish.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: crypto,forex,indicator,ohlc,pandas,stocks,td-countdown,td-sequential,td-setup,td9,tdsequential,tdst,technical-analysis,tom-demark,trading
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Provides-Extra: pandas
|
|
29
|
+
Requires-Dist: pandas>=1.5; extra == 'pandas'
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pandas>=1.5; extra == 'test'
|
|
32
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# TD Sequential indicator for Python
|
|
36
|
+
|
|
37
|
+
[](https://github.com/moolish-com/td-sequential-python/actions/workflows/test.yml)
|
|
38
|
+
[](https://pypi.org/project/td-sequential/)
|
|
39
|
+
[](https://pypi.org/project/td-sequential/)
|
|
40
|
+
[](https://pypi.org/project/td-sequential/)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
|
|
43
|
+
`td-sequential` computes the TD Sequential indicator in Python on a list of OHLC candles: the TD Setup (the 1 to 9 count, often called TD9), the TD Countdown (1 to 13), the TDST support and resistance levels left by completed setups, and the information-only perfection and price flip flags. It works on crypto, stocks and forex candles of any timeframe, has no dependencies, supports Python 3.9 and later, and has an optional pandas helper.
|
|
44
|
+
|
|
45
|
+
It follows the plain-English rules in [SPEC.md](SPEC.md) and matches the reference output bar for bar. The free [TD Sequential counter](https://moolish.com/tools/td-sequential-counter/) on moolish.com shows the current count on a crypto pair without installing anything.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install td-sequential
|
|
51
|
+
pip install "td-sequential[pandas]" # optional DataFrame helper
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from td_sequential import compute, latest
|
|
58
|
+
|
|
59
|
+
# Closed bars only, oldest first. Drop the candle that is still forming.
|
|
60
|
+
bars = [
|
|
61
|
+
{"t": 1750032000000, "o": 105594.02, "h": 108952.38, "l": 104980.37, "c": 106794.53},
|
|
62
|
+
# ...
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
result = compute(bars) # one TdBar record per input bar
|
|
66
|
+
now = latest(result) # summary of the last bar
|
|
67
|
+
|
|
68
|
+
last = result[-1]
|
|
69
|
+
if last.buy_setup == 9:
|
|
70
|
+
print("TD Setup 9 (buy) on", last.t, "resistance", last.resistance)
|
|
71
|
+
if last.sell_countdown == 13:
|
|
72
|
+
print("TD Countdown 13 (sell) completed on", last.t)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A bar can be a dict with keys `t, o, h, l, c` (the long names `time, open, high, low, close` also work), a `(t, o, h, l, c)` tuple, or any object with those attributes, for example a named tuple or a dataclass. Missing fields and non-numeric prices raise a clear `ValueError` or `TypeError` that names the bar.
|
|
76
|
+
|
|
77
|
+
### pandas
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import pandas as pd
|
|
81
|
+
from td_sequential.pandas import add_td_sequential
|
|
82
|
+
|
|
83
|
+
df = pd.read_csv("btcusdt-1d.csv", index_col="time", parse_dates=True) # open, high, low, close columns
|
|
84
|
+
out = add_td_sequential(df) # returns a copy with td_* columns added
|
|
85
|
+
print(out[out["td_buy_setup"] == 9])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Column names are case-insensitive on input. Output columns carry the prefix `td_` (change it with `prefix=`). `td_resistance` and `td_support` are `NaN` when there is no active level.
|
|
89
|
+
|
|
90
|
+
## Output fields
|
|
91
|
+
|
|
92
|
+
`compute()` returns a list of frozen `TdBar` dataclasses, one per input bar, in input order.
|
|
93
|
+
|
|
94
|
+
| Field | Type | Meaning |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `t` | any | The bar's timestamp, copied from the input |
|
|
97
|
+
| `buy_setup` | int | Buy TD Setup counter (0 or more; exactly 9 marks a setup 9) |
|
|
98
|
+
| `sell_setup` | int | Sell TD Setup counter, same meaning |
|
|
99
|
+
| `buy_countdown` | int | Signed buy countdown; 13 means just completed; 14 or more means no countdown in progress; negative means paused |
|
|
100
|
+
| `sell_countdown` | int | Signed sell countdown, same meaning |
|
|
101
|
+
| `resistance` | float or None | Price of the active latest TDST resistance level (plain highest high of the 9 setup bars) |
|
|
102
|
+
| `support` | float or None | Price of the active latest TDST support level (plain lowest low of the 9 setup bars) |
|
|
103
|
+
| `buy_deferred` | bool | The 13th buy countdown bar was held back by the 13-versus-8 test |
|
|
104
|
+
| `sell_deferred` | bool | Same for the sell countdown |
|
|
105
|
+
| `buy_perfected` | bool | Buy setup 9 whose bar 8 or 9 low is strictly below the lows of bars 6 and 7 (information only) |
|
|
106
|
+
| `sell_perfected` | bool | Sell setup 9 whose bar 8 or 9 high is strictly above the highs of bars 6 and 7 (information only) |
|
|
107
|
+
| `bullish_flip` | bool | Price flip: close crosses above the close 4 bars earlier after being below it (information only) |
|
|
108
|
+
| `bearish_flip` | bool | The mirror price flip (information only) |
|
|
109
|
+
|
|
110
|
+
The four information-only fields never change a setup, countdown or level value.
|
|
111
|
+
|
|
112
|
+
`latest(result)` returns a `Latest` dataclass: the current setup counts, the countdown in progress per side (`CountdownState(count, paused)` or `None`), whether a 13 just completed, the time of the last completed setup 9 and countdown 13 per side, and the active resistance and support. It returns `None` for an empty result.
|
|
113
|
+
|
|
114
|
+
## Rules in short
|
|
115
|
+
|
|
116
|
+
- Setup: a bar counts when its close is lower (buy) or higher (sell) than the close 4 bars earlier. Any bar that does not qualify resets the counter to 0. Counting does not stop at 9.
|
|
117
|
+
- Levels: a buy setup 9 records the highest high of its 9 bars as resistance, a sell setup 9 the lowest low as support (the "TDST levels", using the plain 9-bar high and low). A level is dropped when price closes beyond it or a whole bar sits beyond it.
|
|
118
|
+
- Countdown: starts on the setup 9 bar; a bar counts when its close is at or beyond the low (buy) or high (sell) of the bar 2 bars earlier. It needs an active level, is cancelled by the opposite setup 9, and the 13th bar must pass the 13-versus-8 test.
|
|
119
|
+
|
|
120
|
+
The full, unambiguous version is in [SPEC.md](SPEC.md).
|
|
121
|
+
|
|
122
|
+
## Verified against reference data
|
|
123
|
+
|
|
124
|
+
The test suite replays eight real candle histories (crypto pairs on 4-hour, daily and weekly timeframes, about 500 bars each) and requires every counting field to match the stored reference output on every bar. It also compares all twelve fields with the JavaScript implementation of the same rules on the same bars, and has unit cases for setup, levels, countdown, the 13-versus-8 deferral, perfection and price flips. Run it with `pip install -e ".[test]"` then `pytest`.
|
|
125
|
+
|
|
126
|
+
## What is TD Sequential?
|
|
127
|
+
|
|
128
|
+
TD Sequential is Tom DeMark's method for spotting a stretched run of closes. A TD Setup counts consecutive bars that close beyond the close four bars earlier, and a completed 9 can start a countdown that needs 13 qualifying bars. A 9 or a 13 describes past prices and gives no direction on its own; the [TD Sequential guide](https://moolish.com/learn/td-sequential/) explains the method, [TDST](https://moolish.com/learn/td-sequential/tdst/) and [perfected setups](https://moolish.com/learn/td-sequential/perfected-setup/).
|
|
129
|
+
|
|
130
|
+
## Related
|
|
131
|
+
|
|
132
|
+
- [td-sequential](https://github.com/moolish-com/td-sequential) - JavaScript and TypeScript (npm)
|
|
133
|
+
- [td-sequential-pine-script](https://github.com/moolish-com/td-sequential-pine-script) - TradingView Pine Script v6
|
|
134
|
+
- [td-sequential-screener](https://github.com/moolish-com/td-sequential-screener) - command-line screener
|
|
135
|
+
- [Free TD Sequential counter](https://moolish.com/tools/td-sequential-counter/) - check a crypto pair in the browser
|
|
136
|
+
|
|
137
|
+
## Attribution and disclaimer
|
|
138
|
+
|
|
139
|
+
Moolish is not affiliated with, endorsed by or sponsored by Tom DeMark or DeMARK Analytics. TD Sequential is a method created by Tom DeMark, and DeMARK is a trademark of DeMARK Analytics.
|
|
140
|
+
|
|
141
|
+
This is educational software, not financial advice. A count describes past prices; it is not a prediction.
|
|
142
|
+
|
|
143
|
+
## Licence
|
|
144
|
+
|
|
145
|
+
[MIT](LICENSE) - Copyright (c) 2026 Moolish
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
td_sequential/__init__.py,sha256=63ZR6eIVYhKJhMd1en8TahrXL1osRSe9xWI6-et0e9k,256
|
|
2
|
+
td_sequential/core.py,sha256=BxB8vXYLPceI_m7I0OW36G2ElNoBxF1JeCuvsXQJY0I,8705
|
|
3
|
+
td_sequential/pandas.py,sha256=bgM1QWPE4VXKMS1ZRHwN3v2Chj3lpxld-wSJRf53NdY,1929
|
|
4
|
+
td_sequential/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
td_sequential-1.0.0.dist-info/METADATA,sha256=qkJh7KUGFTkfp_pGMaa6ZTYFqwBB0XOtviTePj5Zb5Y,9028
|
|
6
|
+
td_sequential-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
7
|
+
td_sequential-1.0.0.dist-info/licenses/LICENSE,sha256=FmHIfCx4AmcR7l4FIRxNMGlVcrO_WP6SBj3CAoCJxxs,1064
|
|
8
|
+
td_sequential-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moolish
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|