opencode-pyneruntime 6.6.4__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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
pynecore/lib/ta.py
ADDED
|
@@ -0,0 +1,2230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@pyne lib
|
|
3
|
+
"""
|
|
4
|
+
from typing import TypeVar, cast, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from pynecore.types.type_checker import *
|
|
8
|
+
|
|
9
|
+
import builtins
|
|
10
|
+
import math
|
|
11
|
+
import heapq
|
|
12
|
+
|
|
13
|
+
from collections import deque
|
|
14
|
+
|
|
15
|
+
from ..types import Series, Persistent, NA, PyneFloat, PyneInt, PyneBool, na_float
|
|
16
|
+
from ..core.module_property import module_property, module_function_property
|
|
17
|
+
from pynecore.core.overload import overload
|
|
18
|
+
|
|
19
|
+
from ..core import safe_convert
|
|
20
|
+
|
|
21
|
+
# We need to use this kind of import to make transformer work
|
|
22
|
+
from pynecore.lib import (open, high, low, close, volume, hl2, hlc3, bar_index, array, session,
|
|
23
|
+
max_bars_back, math as lib_math)
|
|
24
|
+
|
|
25
|
+
TFIB = TypeVar('TFIB', float, int, bool)
|
|
26
|
+
TFI = TypeVar('TFI', float, int)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"accdist",
|
|
30
|
+
"alma",
|
|
31
|
+
"atr",
|
|
32
|
+
"barssince",
|
|
33
|
+
"bb",
|
|
34
|
+
"bbw",
|
|
35
|
+
"cci",
|
|
36
|
+
"change",
|
|
37
|
+
"cmo",
|
|
38
|
+
"cog",
|
|
39
|
+
"correlation",
|
|
40
|
+
"cross",
|
|
41
|
+
"crossover",
|
|
42
|
+
"crossunder",
|
|
43
|
+
"cum",
|
|
44
|
+
"dev",
|
|
45
|
+
"dmi",
|
|
46
|
+
"ema",
|
|
47
|
+
"falling",
|
|
48
|
+
"highest",
|
|
49
|
+
"highestbars",
|
|
50
|
+
"hma",
|
|
51
|
+
"iii",
|
|
52
|
+
"kc",
|
|
53
|
+
"kcw",
|
|
54
|
+
"linreg",
|
|
55
|
+
"lowest",
|
|
56
|
+
"lowestbars",
|
|
57
|
+
"macd",
|
|
58
|
+
"max",
|
|
59
|
+
"median",
|
|
60
|
+
"mfi",
|
|
61
|
+
"min",
|
|
62
|
+
"mode",
|
|
63
|
+
"mom",
|
|
64
|
+
"nvi",
|
|
65
|
+
"obv",
|
|
66
|
+
"percentile_linear_interpolation",
|
|
67
|
+
"percentile_nearest_rank",
|
|
68
|
+
"percentrank",
|
|
69
|
+
"pivot_point_levels",
|
|
70
|
+
"pivothigh",
|
|
71
|
+
"pivotlow",
|
|
72
|
+
"pvi",
|
|
73
|
+
"pvt",
|
|
74
|
+
"range",
|
|
75
|
+
"rci",
|
|
76
|
+
"rising",
|
|
77
|
+
"rma",
|
|
78
|
+
"roc",
|
|
79
|
+
"rsi",
|
|
80
|
+
"sar",
|
|
81
|
+
"sma",
|
|
82
|
+
"stdev",
|
|
83
|
+
"stoch",
|
|
84
|
+
"supertrend",
|
|
85
|
+
"swma",
|
|
86
|
+
"tr",
|
|
87
|
+
"tsi",
|
|
88
|
+
"valuewhen",
|
|
89
|
+
"variance",
|
|
90
|
+
"vwap",
|
|
91
|
+
"vwma",
|
|
92
|
+
"wad",
|
|
93
|
+
"wma",
|
|
94
|
+
"wpr",
|
|
95
|
+
"wvad"
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# Helper functions
|
|
100
|
+
#
|
|
101
|
+
|
|
102
|
+
EPSILON = 1e-14
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _avgrank(values: list[float], val_to_compare: float) -> float:
|
|
106
|
+
"""
|
|
107
|
+
Helper function to calculate rank with proper tie handling
|
|
108
|
+
|
|
109
|
+
:param values: The list of values
|
|
110
|
+
:param val_to_compare: The value to compare
|
|
111
|
+
:return: The average rank of the value
|
|
112
|
+
"""
|
|
113
|
+
rank_sum = 0.0
|
|
114
|
+
tie_count = 0
|
|
115
|
+
|
|
116
|
+
# Count values less than current and ties
|
|
117
|
+
for vi in values:
|
|
118
|
+
diff = vi - val_to_compare
|
|
119
|
+
if diff > EPSILON:
|
|
120
|
+
rank_sum += 1
|
|
121
|
+
elif abs(diff) < EPSILON:
|
|
122
|
+
tie_count += 1
|
|
123
|
+
|
|
124
|
+
# Return average rank for ties
|
|
125
|
+
return rank_sum + (tie_count - 1) / 2.0 + 1
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
#
|
|
129
|
+
# Indicators
|
|
130
|
+
#
|
|
131
|
+
|
|
132
|
+
@module_property
|
|
133
|
+
def accdist() -> PyneFloat:
|
|
134
|
+
"""
|
|
135
|
+
Accumulation/Distribution index
|
|
136
|
+
A/D = ((Close - Low) - (High - Close)) / (High - Low) * Volume + Previous A/D
|
|
137
|
+
|
|
138
|
+
:return: Accumulation/Distribution index
|
|
139
|
+
"""
|
|
140
|
+
ad: Persistent[float] = 0.0
|
|
141
|
+
|
|
142
|
+
mfm = ((close - low) - (high - close)) / (high - low)
|
|
143
|
+
mfv = mfm * volume
|
|
144
|
+
if not (isinstance(mfv, NA) or mfv != mfv):
|
|
145
|
+
ad += mfv
|
|
146
|
+
|
|
147
|
+
return ad
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def alma(source: Series[float], length: int, offset: float = 0.85, sigma: float = 6.0, floor=False) \
|
|
151
|
+
-> PyneFloat:
|
|
152
|
+
"""
|
|
153
|
+
Calculate the Arnaud Legoux Moving Average (ALMA) of the source series with the given length.
|
|
154
|
+
|
|
155
|
+
Fun fact: ALMA means "soul" in latin and Spanish, and Portugese.
|
|
156
|
+
It means "apple" in Hungarian, Finnish, and Estonian.
|
|
157
|
+
It means "take it" in Turkish.
|
|
158
|
+
It means "water" in Arabic.
|
|
159
|
+
It means "apple tree" in Georgian.
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
:param source: The source series
|
|
163
|
+
:param length: The length of the ALMA
|
|
164
|
+
:param offset: The offset of the ALMA
|
|
165
|
+
:param sigma: The sigma value of the ALMA
|
|
166
|
+
:param floor: Specifies whether the offset calculation is floored before ALMA is calculated. Default value is false
|
|
167
|
+
:return: The ALMA of the source series
|
|
168
|
+
"""
|
|
169
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
170
|
+
if (isinstance(source, NA) or source != source):
|
|
171
|
+
return na_float
|
|
172
|
+
length = int(length)
|
|
173
|
+
|
|
174
|
+
# Use persistent weights to avoid recalculation
|
|
175
|
+
weights: Persistent[list[float]] = []
|
|
176
|
+
norm: Persistent[float] = 0.0
|
|
177
|
+
|
|
178
|
+
# Calculate weights only once
|
|
179
|
+
if not weights:
|
|
180
|
+
m = offset * (length - 1) if not floor else math.floor(offset * (length - 1))
|
|
181
|
+
s = length / sigma
|
|
182
|
+
weights = [math.exp(-1 * ((i - m) * (i - m)) / (2 * s * s)) for i in builtins.range(length)]
|
|
183
|
+
weights.reverse() # This is faster then using backward range or index subtraction
|
|
184
|
+
norm = sum(weights)
|
|
185
|
+
|
|
186
|
+
# Vectorized calculation using dot product
|
|
187
|
+
summ = 0.0
|
|
188
|
+
for i, w in enumerate(weights):
|
|
189
|
+
summ += w * source[i]
|
|
190
|
+
return summ / norm
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def atr(length: int) -> PyneFloat:
|
|
194
|
+
"""
|
|
195
|
+
Calculate Average True Range (ATR) of the source series with the given length.
|
|
196
|
+
|
|
197
|
+
:param length: The length of the ATR
|
|
198
|
+
:return: The ATR of the source series
|
|
199
|
+
"""
|
|
200
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
201
|
+
return rma(tr(True), length)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def barssince(condition: bool) -> PyneInt:
|
|
205
|
+
"""
|
|
206
|
+
Calculate the number of bars since the condition was true.
|
|
207
|
+
|
|
208
|
+
:param condition: The condition to check
|
|
209
|
+
:return: The number of bars since the condition was true
|
|
210
|
+
"""
|
|
211
|
+
counter: Persistent[int] = -1
|
|
212
|
+
if condition:
|
|
213
|
+
counter = 0
|
|
214
|
+
elif counter == -1:
|
|
215
|
+
return NA(int)
|
|
216
|
+
else:
|
|
217
|
+
counter += 1
|
|
218
|
+
return counter
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def bb(source: float, length: int, mult: float | int) -> tuple[PyneFloat, PyneFloat, PyneFloat]:
|
|
222
|
+
"""
|
|
223
|
+
Calculate the Bollinger Bands (BB) of the source series with the given length and multiplier.
|
|
224
|
+
|
|
225
|
+
:param source: The source series
|
|
226
|
+
:param length: The length of the BB
|
|
227
|
+
:param mult: The multiplier of the BB
|
|
228
|
+
:return: The Bollinger Bands (BB) of the source series
|
|
229
|
+
"""
|
|
230
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
231
|
+
assert mult > 0, "Invalid multiplier, multiplier must be greater than 0!"
|
|
232
|
+
|
|
233
|
+
std_dev = stdev(source, length)
|
|
234
|
+
|
|
235
|
+
middle = sma(source, length)
|
|
236
|
+
|
|
237
|
+
if (isinstance(middle, NA) or middle != middle):
|
|
238
|
+
return na_float, na_float, na_float
|
|
239
|
+
std_dev *= mult
|
|
240
|
+
return middle, middle + std_dev, middle - std_dev
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def bbw(source: float, length: int, mult: float | int) -> PyneFloat:
|
|
244
|
+
"""
|
|
245
|
+
Calculate the Bollinger Bands Width (BBW) of the source series with the given length and multiplier.
|
|
246
|
+
|
|
247
|
+
:param source: The source series
|
|
248
|
+
:param length: The length of the BBW
|
|
249
|
+
:param mult: The multiplier of the BBW
|
|
250
|
+
:return: The Bollinger Bands Width (BBW) of the source series
|
|
251
|
+
"""
|
|
252
|
+
b, h, l = bb(source, length, mult)
|
|
253
|
+
if (isinstance(b, NA) or b != b) or b == 0.0:
|
|
254
|
+
return na_float
|
|
255
|
+
return ((h - l) / b) * 100
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def cci(source: float, length: int) -> PyneFloat:
|
|
259
|
+
"""
|
|
260
|
+
Calculate the Commodity Channel Index (CCI) of the source series with the given length.
|
|
261
|
+
|
|
262
|
+
:param source: The source series
|
|
263
|
+
:param length: The length of the CCI
|
|
264
|
+
:return: The Commodity Channel Index (CCI) of the source series
|
|
265
|
+
"""
|
|
266
|
+
mean = sma(source, length)
|
|
267
|
+
mdev = dev(source, length, _mean=mean)
|
|
268
|
+
if (isinstance(mdev, NA) or mdev != mdev):
|
|
269
|
+
return na_float
|
|
270
|
+
return (source - mean) / (0.015 * mdev)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def change(source: Series[TFIB], length: int = 1) -> TFIB:
|
|
274
|
+
"""
|
|
275
|
+
Calculate a simple change with respect to the given bar offset.
|
|
276
|
+
|
|
277
|
+
:param source: The source series
|
|
278
|
+
:param length: The offset in bars
|
|
279
|
+
:return: The change from source to source[length]
|
|
280
|
+
"""
|
|
281
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
282
|
+
length = int(length)
|
|
283
|
+
# Grow the buffer so ``source[length]`` stays addressable for lengths beyond the
|
|
284
|
+
# per-series default max_bars_back (500); otherwise it reads na and the change is na.
|
|
285
|
+
max_bars_back(source, length)
|
|
286
|
+
|
|
287
|
+
# We need to round to prevent problems caused by floating point precision
|
|
288
|
+
if isinstance(source, (float, int)):
|
|
289
|
+
# The cast is for pyright: it types round(float) as float, which is not
|
|
290
|
+
# assignable back to the TFIB-typed parameter; PyCharm resolves it to
|
|
291
|
+
# TFIB already.
|
|
292
|
+
# noinspection PyUnnecessaryCast
|
|
293
|
+
source = cast(TFIB, round(source, 14))
|
|
294
|
+
prev_val = source[length] # noqa
|
|
295
|
+
|
|
296
|
+
if (isinstance(source, NA) or source != source):
|
|
297
|
+
# type(source) would be the NA class itself — keep the source sentinel's type
|
|
298
|
+
return cast(TFIB, source)
|
|
299
|
+
if (isinstance(prev_val, NA) or prev_val != prev_val):
|
|
300
|
+
return NA(type(source))
|
|
301
|
+
if isinstance(source, float):
|
|
302
|
+
return cast(TFIB, round(source - prev_val, 14)) # noqa
|
|
303
|
+
if isinstance(source, int):
|
|
304
|
+
return cast(TFIB, source - prev_val) # noqa
|
|
305
|
+
return source != prev_val
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cmo(source: float, length: int) -> PyneFloat:
|
|
309
|
+
"""
|
|
310
|
+
Calculate the Chande Momentum Oscillator (CMO) of the source series with the given length.
|
|
311
|
+
|
|
312
|
+
:param source: The source series
|
|
313
|
+
:param length: The length of the CMO
|
|
314
|
+
:return: The Chande Momentum Oscillator (CMO) of the source series
|
|
315
|
+
"""
|
|
316
|
+
momentum = change(source)
|
|
317
|
+
if (isinstance(momentum, NA) or momentum != momentum):
|
|
318
|
+
return na_float
|
|
319
|
+
sum1 = lib_math.sum(momentum if momentum >= 0.0 else 0.0, length)
|
|
320
|
+
sum2 = lib_math.sum(0.0 if momentum >= 0.0 else -momentum, length)
|
|
321
|
+
return 100 * (sum1 - sum2) / (sum1 + sum2)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# noinspection PyUnusedLocal,PyShadowingBuiltins
|
|
325
|
+
def cog(source: Series[float], length: int) -> PyneFloat:
|
|
326
|
+
"""
|
|
327
|
+
Calculate the Center of Gravity (COG) of the source series with the given length.
|
|
328
|
+
|
|
329
|
+
:param source: The source series
|
|
330
|
+
:param length: The length of the COG
|
|
331
|
+
:return: The Center of Gravity (COG) of the source series
|
|
332
|
+
"""
|
|
333
|
+
count: Persistent[int] = 0
|
|
334
|
+
summ: Persistent[float] = 0.0
|
|
335
|
+
weighted_summ: Persistent[float] = 0.0
|
|
336
|
+
val: Persistent[float] = na_float
|
|
337
|
+
|
|
338
|
+
if (isinstance(source, NA) or source != source):
|
|
339
|
+
# An NA bar leaves the window unchanged; hold the last full value
|
|
340
|
+
# (still NA while warming up)
|
|
341
|
+
return na_float if count < length else val
|
|
342
|
+
|
|
343
|
+
# NA values are NOT stored in the buffer, only skipped, so ``src[length]``
|
|
344
|
+
# indexes past NA gaps to the true oldest value still inside the window.
|
|
345
|
+
# Reading the parameter directly would step back ``length`` *bars* and land
|
|
346
|
+
# inside an NA gap, subtracting an NA that poisons ``summ`` forever.
|
|
347
|
+
src: Series[float] = source
|
|
348
|
+
# Grow the na-compacted buffer so ``src[length]`` stays addressable for lengths
|
|
349
|
+
# beyond the per-series default max_bars_back (500); otherwise the window-drop
|
|
350
|
+
# read returns na and poisons ``summ`` permanently.
|
|
351
|
+
max_bars_back(src, int(length))
|
|
352
|
+
|
|
353
|
+
# Warming up phase — only non-NA samples advance the window
|
|
354
|
+
if count < length:
|
|
355
|
+
count += 1
|
|
356
|
+
summ += source
|
|
357
|
+
weighted_summ += source * (length - count)
|
|
358
|
+
if count < length:
|
|
359
|
+
return na_float
|
|
360
|
+
|
|
361
|
+
# Normal calculation phase
|
|
362
|
+
else:
|
|
363
|
+
new_summ = summ + source - src[length]
|
|
364
|
+
weighted_summ = weighted_summ + summ - length * src[length]
|
|
365
|
+
summ = new_summ
|
|
366
|
+
val = -weighted_summ / summ - 1.0
|
|
367
|
+
return val
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def correlation(source1: Series[float], source2: Series[float], length: int) -> PyneFloat:
|
|
371
|
+
"""
|
|
372
|
+
Calculate the correlation of the source series with the given length.
|
|
373
|
+
|
|
374
|
+
NOTE: It is about 7 digits accurate to the result of Pine Script's correlation function. There
|
|
375
|
+
are a lot of floating point operations, and even the order matters. I cannot found a
|
|
376
|
+
better matching calculation.
|
|
377
|
+
|
|
378
|
+
:param source1: The first source series
|
|
379
|
+
:param source2: The second source series
|
|
380
|
+
:param length: The length of the correlation
|
|
381
|
+
:return: The correlation of the source series
|
|
382
|
+
"""
|
|
383
|
+
assert length > 0, "Length must be greater than 0"
|
|
384
|
+
length = int(length)
|
|
385
|
+
# Grow both buffers so ``source1/source2[length]`` stay addressable for lengths beyond
|
|
386
|
+
# the per-series default max_bars_back (500); otherwise the window-drop reads na and
|
|
387
|
+
# poisons the running sums permanently.
|
|
388
|
+
max_bars_back(source1, length)
|
|
389
|
+
max_bars_back(source2, length)
|
|
390
|
+
|
|
391
|
+
sum_x: Persistent[float] = 0.0
|
|
392
|
+
sum_y: Persistent[float] = 0.0
|
|
393
|
+
sum_xy: Persistent[float] = 0.0
|
|
394
|
+
sum_x2: Persistent[float] = 0.0
|
|
395
|
+
sum_y2: Persistent[float] = 0.0
|
|
396
|
+
count: Persistent[int] = 0
|
|
397
|
+
na_window: Persistent[int] = 0
|
|
398
|
+
|
|
399
|
+
cur_na = (isinstance(source1, NA) or source1 != source1) or (isinstance(source2, NA) or source2 != source2)
|
|
400
|
+
|
|
401
|
+
if count < length:
|
|
402
|
+
# Still filling the window; no bar leaves it yet.
|
|
403
|
+
if cur_na:
|
|
404
|
+
na_window += 1
|
|
405
|
+
else:
|
|
406
|
+
sum_x += source1
|
|
407
|
+
sum_y += source2
|
|
408
|
+
sum_xy += source1 * source2
|
|
409
|
+
sum_x2 += source1 * source1
|
|
410
|
+
sum_y2 += source2 * source2
|
|
411
|
+
count += 1
|
|
412
|
+
if count < length:
|
|
413
|
+
return na_float
|
|
414
|
+
else:
|
|
415
|
+
old1 = source1[length]
|
|
416
|
+
old2 = source2[length]
|
|
417
|
+
old_na = (isinstance(old1, NA) or old1 != old1) or (isinstance(old2, NA) or old2 != old2)
|
|
418
|
+
if not cur_na and not old_na:
|
|
419
|
+
# na-free fast path: same operations and order as before, to keep the
|
|
420
|
+
# documented ~7-digit match with Pine's correlation.
|
|
421
|
+
sum_x += source1 - old1
|
|
422
|
+
sum_y += source2 - old2
|
|
423
|
+
sum_xy += (source1 * source2) - (old1 * old2)
|
|
424
|
+
sum_x2 += (source1 * source1) - (old1 * old1)
|
|
425
|
+
sum_y2 += (source2 * source2) - (old2 * old2)
|
|
426
|
+
else:
|
|
427
|
+
# A na entered or left the window: keep the running sums over the
|
|
428
|
+
# valid bars only and track how many na sit in the window.
|
|
429
|
+
if cur_na:
|
|
430
|
+
na_window += 1
|
|
431
|
+
else:
|
|
432
|
+
sum_x += source1
|
|
433
|
+
sum_y += source2
|
|
434
|
+
sum_xy += source1 * source2
|
|
435
|
+
sum_x2 += source1 * source1
|
|
436
|
+
sum_y2 += source2 * source2
|
|
437
|
+
if old_na:
|
|
438
|
+
na_window -= 1
|
|
439
|
+
else:
|
|
440
|
+
sum_x -= old1
|
|
441
|
+
sum_y -= old2
|
|
442
|
+
sum_xy -= old1 * old2
|
|
443
|
+
sum_x2 -= old1 * old1
|
|
444
|
+
sum_y2 -= old2 * old2
|
|
445
|
+
|
|
446
|
+
if na_window:
|
|
447
|
+
return na_float
|
|
448
|
+
try:
|
|
449
|
+
numerator = (length * sum_xy) - (sum_x * sum_y)
|
|
450
|
+
denominator = math.sqrt((length * sum_x2 - sum_x * sum_x) * (length * sum_y2 - sum_y * sum_y))
|
|
451
|
+
return numerator / denominator
|
|
452
|
+
except (ValueError, ZeroDivisionError):
|
|
453
|
+
return na_float
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def cross(source1: float, source2: float) -> PyneBool:
|
|
457
|
+
"""
|
|
458
|
+
Check if the source series crossed over or under the given series.
|
|
459
|
+
|
|
460
|
+
:param source1: The first source series
|
|
461
|
+
:param source2: The second source series
|
|
462
|
+
:return: True if the source series crossed over the given series
|
|
463
|
+
"""
|
|
464
|
+
return crossover(source1, source2) or crossunder(source1, source2)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# noinspection PyUnusedLocal
|
|
468
|
+
def crossover(source1: float, source2: float) -> PyneBool:
|
|
469
|
+
"""
|
|
470
|
+
Check if the source series crossed over the given series.
|
|
471
|
+
|
|
472
|
+
:param source1: The first source series
|
|
473
|
+
:param source2: The second source series
|
|
474
|
+
:return: True if the source series crossed over the given series
|
|
475
|
+
"""
|
|
476
|
+
l1_lte_l2: Persistent[bool] = NA(bool)
|
|
477
|
+
res = source1 > source2 and l1_lte_l2
|
|
478
|
+
# Only refresh the relation on bars where it is defined; TV compares against the
|
|
479
|
+
# last bar with both sources present, so na gaps must not reset the state
|
|
480
|
+
if not ((isinstance(source1, NA) or source1 != source1) or (isinstance(source2, NA) or source2 != source2)):
|
|
481
|
+
l1_lte_l2 = source1 <= source2
|
|
482
|
+
return res
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# noinspection PyUnusedLocal
|
|
486
|
+
def crossunder(source1: float, source2: float) -> PyneBool:
|
|
487
|
+
"""
|
|
488
|
+
Check if the source series crossed under the given series.
|
|
489
|
+
|
|
490
|
+
:param source1: The first source series
|
|
491
|
+
:param source2: The second source series
|
|
492
|
+
:return: True if the source series crossed under the given series
|
|
493
|
+
"""
|
|
494
|
+
l1_gte_l2: Persistent[bool] = NA(bool)
|
|
495
|
+
res = source1 < source2 and l1_gte_l2
|
|
496
|
+
# Only refresh the relation on bars where it is defined; TV compares against the
|
|
497
|
+
# last bar with both sources present, so na gaps must not reset the state
|
|
498
|
+
if not ((isinstance(source1, NA) or source1 != source1) or (isinstance(source2, NA) or source2 != source2)):
|
|
499
|
+
l1_gte_l2 = source1 >= source2
|
|
500
|
+
return res
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def cum(source: Series[float | int]) -> PyneFloat:
|
|
504
|
+
"""
|
|
505
|
+
Calculate the cumulative sum of the source series.
|
|
506
|
+
|
|
507
|
+
:param source: The source series
|
|
508
|
+
:return: The cumulative sum of the source series
|
|
509
|
+
"""
|
|
510
|
+
if (isinstance(source, NA) or source != source):
|
|
511
|
+
return na_float
|
|
512
|
+
var: Persistent[float] = 0.0
|
|
513
|
+
var += source
|
|
514
|
+
return var
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def dev(source: Series[float], length: int, _mean: PyneFloat | None = None) -> PyneFloat:
|
|
518
|
+
"""
|
|
519
|
+
Calculate the Mean Absolute Deviation (MAD) of the source series with the given length.
|
|
520
|
+
|
|
521
|
+
:param source: The source series
|
|
522
|
+
:param length: The length of the MAD calculation
|
|
523
|
+
:param _mean: The mean value of the source series, if it is already calculated
|
|
524
|
+
:return: The mean absolute deviation of the source series
|
|
525
|
+
"""
|
|
526
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
527
|
+
if length == 1:
|
|
528
|
+
return 0.0
|
|
529
|
+
length = int(length)
|
|
530
|
+
|
|
531
|
+
mean = _mean if _mean is not None else sma(source, length)
|
|
532
|
+
if (isinstance(mean, NA) or mean != mean):
|
|
533
|
+
return na_float
|
|
534
|
+
|
|
535
|
+
summ = 0.0
|
|
536
|
+
for i in builtins.range(length):
|
|
537
|
+
summ += abs(source[i] - mean)
|
|
538
|
+
|
|
539
|
+
return summ / length
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# noinspection PyPep8Naming
|
|
543
|
+
def dmi(diLength: int, adxSmoothing: int) -> tuple[PyneFloat, PyneFloat, PyneFloat]:
|
|
544
|
+
"""
|
|
545
|
+
Calculate the Directional Movement Index (DMI) of the source series with the given DI length and ADX smoothing.
|
|
546
|
+
|
|
547
|
+
:param diLength: The length of the DI
|
|
548
|
+
:param adxSmoothing: The smoothing of the ADX
|
|
549
|
+
:return: Tuple of three DMI series:
|
|
550
|
+
- Positive Directional Movement (+DI)
|
|
551
|
+
- Negative Directional Movement (-DI)
|
|
552
|
+
- Average Directional Movement Index (ADX)
|
|
553
|
+
"""
|
|
554
|
+
assert diLength > 0, "Invalid DI length, DI length must be greater than 0!"
|
|
555
|
+
assert adxSmoothing > 0, "Invalid ADX smoothing, ADX smoothing must be greater than 0!"
|
|
556
|
+
up = change(high)
|
|
557
|
+
down = -change(low)
|
|
558
|
+
if (isinstance(up, NA) or up != up) or (isinstance(down, NA) or down != down):
|
|
559
|
+
return na_float, na_float, na_float
|
|
560
|
+
a = atr(diLength)
|
|
561
|
+
plus_dm = up if (up > down and up > 0.0) else 0.0
|
|
562
|
+
minus_dm = down if (down > up and down > 0.0) else 0.0
|
|
563
|
+
p = rma(plus_dm, diLength)
|
|
564
|
+
m = rma(minus_dm, diLength)
|
|
565
|
+
if (isinstance(a, NA) or a != a) or (isinstance(p, NA) or p != p) or (isinstance(m, NA) or m != m) or a == 0.0:
|
|
566
|
+
return na_float, na_float, na_float
|
|
567
|
+
p = 100 * p / a
|
|
568
|
+
m = 100 * m / a
|
|
569
|
+
summ = p + m
|
|
570
|
+
adx = rma(abs(p - m) / (summ if summ != 0.0 else 1.0), adxSmoothing) * 100
|
|
571
|
+
return p, m, adx
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def ema(source: PyneFloat, length: int, _alpha: float | None = None) -> PyneFloat:
|
|
575
|
+
"""
|
|
576
|
+
Calculate the Exponential Moving Average (EMA) of the source series with the given length.
|
|
577
|
+
|
|
578
|
+
:param source: The source series
|
|
579
|
+
:param length: The length of the EMA
|
|
580
|
+
:param _alpha: The alpha value for EMA calculation (it is a private argument)
|
|
581
|
+
:return:
|
|
582
|
+
"""
|
|
583
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
584
|
+
length = int(length)
|
|
585
|
+
if length == 1: # Shortcut
|
|
586
|
+
return source
|
|
587
|
+
|
|
588
|
+
if (isinstance(source, NA) or source != source):
|
|
589
|
+
return na_float
|
|
590
|
+
|
|
591
|
+
alpha: Persistent[float] = _alpha or (2 / (length + 1))
|
|
592
|
+
last_val: Persistent[float] = na_float
|
|
593
|
+
|
|
594
|
+
# Use SMA at warming stage
|
|
595
|
+
if (isinstance(last_val, NA) or last_val != last_val):
|
|
596
|
+
last_val = sma(source, length)
|
|
597
|
+
return last_val
|
|
598
|
+
|
|
599
|
+
# Warmed result
|
|
600
|
+
last_val = alpha * source + (1 - alpha) * last_val
|
|
601
|
+
return last_val
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# noinspection PyUnusedLocal
|
|
605
|
+
def falling(source: float, length: int) -> bool:
|
|
606
|
+
"""
|
|
607
|
+
Test if the source series is now falling for length bars long.
|
|
608
|
+
|
|
609
|
+
:param source: The source series
|
|
610
|
+
:param length: The length of the falling test
|
|
611
|
+
:return: True if the source series is falling for length bars long
|
|
612
|
+
"""
|
|
613
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
614
|
+
length = int(length)
|
|
615
|
+
|
|
616
|
+
last_val: Persistent[float] = na_float
|
|
617
|
+
counter: Persistent[int] = 0
|
|
618
|
+
|
|
619
|
+
if (isinstance(last_val, NA) or last_val != last_val):
|
|
620
|
+
last_val = source
|
|
621
|
+
return False
|
|
622
|
+
|
|
623
|
+
if source < last_val:
|
|
624
|
+
counter += 1
|
|
625
|
+
else:
|
|
626
|
+
counter = 0
|
|
627
|
+
|
|
628
|
+
last_val = source
|
|
629
|
+
return counter >= length
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# noinspection PyUnusedLocal,DuplicatedCode
|
|
633
|
+
@overload
|
|
634
|
+
def highest(source: Series[float], length: int, _bars: bool = False, _tuple: bool = False, _check_eq: bool = False) \
|
|
635
|
+
-> PyneFloat:
|
|
636
|
+
"""
|
|
637
|
+
Calculate the highest value of the source series with the given length.
|
|
638
|
+
|
|
639
|
+
:param source: The source series
|
|
640
|
+
:param length: The length of the highest value
|
|
641
|
+
:param _bars: If true, return the number of bars since the highest value, internal use only
|
|
642
|
+
:param _tuple: If true, return a tuple of the highest value and the number of bars since the highest value,
|
|
643
|
+
internal use only
|
|
644
|
+
:param _check_eq: If true, check for equality too, internal use only
|
|
645
|
+
:return: The highest value of the source series
|
|
646
|
+
"""
|
|
647
|
+
last_max: Persistent[float] = na_float
|
|
648
|
+
last_max_index: Persistent[int] = 0
|
|
649
|
+
|
|
650
|
+
if last_max < source or (isinstance(last_max, NA) or last_max != last_max) or (_check_eq and last_max == source):
|
|
651
|
+
last_max = source
|
|
652
|
+
last_max_index = 0
|
|
653
|
+
|
|
654
|
+
if last_max_index >= length:
|
|
655
|
+
last_max = source
|
|
656
|
+
last_max_index = 0
|
|
657
|
+
for i in builtins.range(1, length):
|
|
658
|
+
s = source[i]
|
|
659
|
+
if s > last_max:
|
|
660
|
+
last_max = s
|
|
661
|
+
last_max_index = i
|
|
662
|
+
elif not _check_eq and s == last_max:
|
|
663
|
+
# For normal highest: update index for equal values
|
|
664
|
+
last_max_index = i
|
|
665
|
+
# For pivot detection (_check_eq=True): don't update index for equal values
|
|
666
|
+
|
|
667
|
+
max_index = last_max_index
|
|
668
|
+
last_max_index += 1
|
|
669
|
+
|
|
670
|
+
if bar_index < length - 1:
|
|
671
|
+
return na_float if not _tuple else (na_float, na_float) # type: ignore[return-value]
|
|
672
|
+
|
|
673
|
+
if _bars:
|
|
674
|
+
return -max_index
|
|
675
|
+
if _tuple:
|
|
676
|
+
return last_max, -max_index # type: ignore[return-value]
|
|
677
|
+
return last_max
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@overload
|
|
681
|
+
def highest(length: int) -> PyneFloat:
|
|
682
|
+
return highest(high, length)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# noinspection PyUnusedLocal
|
|
686
|
+
@overload
|
|
687
|
+
def highestbars(source: Series[float], length: int) -> PyneFloat:
|
|
688
|
+
"""
|
|
689
|
+
Calculate the number of bars since the highest value of the source series with the given length.
|
|
690
|
+
|
|
691
|
+
:param source: The source series
|
|
692
|
+
:param length: The length of the highest value
|
|
693
|
+
:return: The number of bars since the highest value of the source series
|
|
694
|
+
"""
|
|
695
|
+
return highest(source, length, _bars=True)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@overload
|
|
699
|
+
def highestbars(length: int) -> PyneFloat:
|
|
700
|
+
return highest(high, length, _bars=True)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def hma(source: float, length: int) -> PyneFloat:
|
|
704
|
+
"""
|
|
705
|
+
Calculate the Hull Moving Average (HMA) of the source series with the given length.
|
|
706
|
+
|
|
707
|
+
:param source: The source series
|
|
708
|
+
:param length: The length of the HMA
|
|
709
|
+
:return: The Hull Moving Average (HMA) of the source series
|
|
710
|
+
"""
|
|
711
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
712
|
+
if (isinstance(source, NA) or source != source):
|
|
713
|
+
return na_float
|
|
714
|
+
length = int(length)
|
|
715
|
+
|
|
716
|
+
ma_np2 = wma(source, length // 2)
|
|
717
|
+
ma = wma(source, length)
|
|
718
|
+
if (isinstance(ma, NA) or ma != ma) or (isinstance(ma_np2, NA) or ma_np2 != ma_np2):
|
|
719
|
+
return na_float
|
|
720
|
+
return wma(2 * ma_np2 - ma, int(length ** 0.5))
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@module_property
|
|
724
|
+
def iii() -> PyneFloat:
|
|
725
|
+
"""
|
|
726
|
+
Intraday Intensity Index.
|
|
727
|
+
|
|
728
|
+
:return: Intraday Intensity Index
|
|
729
|
+
"""
|
|
730
|
+
return (2 * close - high - low) / ((high - low) * volume)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# noinspection PyPep8Naming
|
|
734
|
+
def kc(series: float, length: int, mult: float | int, useTrueRange: bool = True) \
|
|
735
|
+
-> tuple[PyneFloat, PyneFloat, PyneFloat]:
|
|
736
|
+
"""
|
|
737
|
+
Calculate the Keltner Channels (KC) of the source series with the given length and multiplier.
|
|
738
|
+
|
|
739
|
+
:param series: The source series
|
|
740
|
+
:param length: The length of the KC
|
|
741
|
+
:param mult: The multiplier of the KC
|
|
742
|
+
:param useTrueRange: Specifies whether to use True Range for KC calculation
|
|
743
|
+
:return: The Keltner Channels (KC) of the source series
|
|
744
|
+
"""
|
|
745
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
746
|
+
assert mult > 0, "Invalid multiplier, multiplier must be greater than 0!"
|
|
747
|
+
|
|
748
|
+
base = ema(series, length)
|
|
749
|
+
span = tr(False) if useTrueRange else (high - low)
|
|
750
|
+
range_ma = ema(span, length)
|
|
751
|
+
if (isinstance(base, NA) or base != base):
|
|
752
|
+
return na_float, na_float, na_float
|
|
753
|
+
if (isinstance(range_ma, NA) or range_ma != range_ma):
|
|
754
|
+
return base, na_float, na_float
|
|
755
|
+
range_ma *= mult
|
|
756
|
+
return base, base + range_ma, base - range_ma
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
# noinspection PyPep8Naming
|
|
760
|
+
def kcw(series: float, length: int, mult: float | int, useTrueRange: bool = True) -> PyneFloat:
|
|
761
|
+
"""
|
|
762
|
+
Calculate the Keltner Channels Width (KCW) of the source series with the given length and multiplier.
|
|
763
|
+
|
|
764
|
+
:param series: The source series
|
|
765
|
+
:param length: The length of the KCW
|
|
766
|
+
:param mult: The multiplier of the KCW
|
|
767
|
+
:param useTrueRange: Specifies whether to use True Range for KCW calculation
|
|
768
|
+
:return: The Keltner Channels Width (KCW) of the source series
|
|
769
|
+
"""
|
|
770
|
+
b, h, l = kc(series, length, mult, useTrueRange)
|
|
771
|
+
if (isinstance(b, NA) or b != b) or b == 0.0:
|
|
772
|
+
return na_float
|
|
773
|
+
return (h - l) / b
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def linreg(source: Series[float], length: int, offset: int) -> PyneFloat:
|
|
777
|
+
"""
|
|
778
|
+
Computes the linear regression value of the source series over a given period.
|
|
779
|
+
|
|
780
|
+
:param source: Input series
|
|
781
|
+
:param length: Number of bars to calculate regression
|
|
782
|
+
:param offset: Number of bars to shift the result
|
|
783
|
+
:return: Linear regression value
|
|
784
|
+
"""
|
|
785
|
+
assert length > 0, "Invalid length, must be greater than 0!"
|
|
786
|
+
if length == 1:
|
|
787
|
+
return source
|
|
788
|
+
length = int(length)
|
|
789
|
+
window_size = length
|
|
790
|
+
|
|
791
|
+
# Precomputed constants for x-coordinates
|
|
792
|
+
sum_x = window_size * (window_size - 1) / 2.0
|
|
793
|
+
sum_x2 = (window_size - 1) * window_size * (2 * window_size - 1) / 6.0
|
|
794
|
+
|
|
795
|
+
# Persistent state variables
|
|
796
|
+
bar_count: Persistent[int] = 0
|
|
797
|
+
sum_y: Persistent[float] = 0.0 # Sum of source values in the window
|
|
798
|
+
sum_xy: Persistent[float] = 0.0 # Weighted sum: sum((window_size - 1 - i) * source[i])
|
|
799
|
+
|
|
800
|
+
if (isinstance(source, NA) or source != source):
|
|
801
|
+
if bar_count < window_size:
|
|
802
|
+
return na_float
|
|
803
|
+
# An NA bar leaves the window unchanged; fall through to report the held value
|
|
804
|
+
else:
|
|
805
|
+
# NA values are NOT stored in the buffer, only skipped, so ``src[window_size]``
|
|
806
|
+
# indexes past NA gaps to the true oldest value still inside the window.
|
|
807
|
+
# Reading the parameter directly would step back ``window_size`` *bars* and
|
|
808
|
+
# land inside an NA gap, subtracting an NA that poisons ``sum_y`` forever.
|
|
809
|
+
src: Series[float] = source
|
|
810
|
+
|
|
811
|
+
# Warm-up phase: accumulate values until the window is full
|
|
812
|
+
if bar_count < window_size:
|
|
813
|
+
prev_sum_y = sum_y
|
|
814
|
+
sum_y = prev_sum_y + source
|
|
815
|
+
sum_xy = (window_size - 1) * source + sum_xy - prev_sum_y
|
|
816
|
+
bar_count += 1
|
|
817
|
+
|
|
818
|
+
# Return NA until we have enough data
|
|
819
|
+
if bar_count < window_size:
|
|
820
|
+
return na_float
|
|
821
|
+
else:
|
|
822
|
+
# Rolling update: remove the oldest value when the window is full
|
|
823
|
+
dropped_value = src[window_size]
|
|
824
|
+
prev_sum_y = sum_y
|
|
825
|
+
sum_y = prev_sum_y + source - dropped_value
|
|
826
|
+
sum_xy = (window_size - 1) * source + sum_xy - prev_sum_y + dropped_value
|
|
827
|
+
|
|
828
|
+
# Compute slope and intercept
|
|
829
|
+
denominator = window_size * sum_x2 - sum_x * sum_x
|
|
830
|
+
slope = (window_size * sum_xy - sum_x * sum_y) / denominator
|
|
831
|
+
intercept = (sum_y - slope * sum_x) / window_size
|
|
832
|
+
|
|
833
|
+
# Compute final regression value
|
|
834
|
+
return intercept + slope * ((window_size - 1) - offset)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
# noinspection PyUnusedLocal,DuplicatedCode
|
|
838
|
+
@overload
|
|
839
|
+
def lowest(source: Series[float], length: int,
|
|
840
|
+
_bars: bool = False, _tuple: bool = False, _check_eq: bool = False) \
|
|
841
|
+
-> PyneFloat:
|
|
842
|
+
"""
|
|
843
|
+
Calculate the lowest value of the source series with the given length.
|
|
844
|
+
|
|
845
|
+
:param source: The source series
|
|
846
|
+
:param length: The length of the lowest value
|
|
847
|
+
:param _bars: If true, return the number of bars since the lowest value, internal use only
|
|
848
|
+
:param _tuple: If true, return a tuple of the lowest value and the number of bars since the lowest value,
|
|
849
|
+
Internal use only
|
|
850
|
+
:param _check_eq: If true, check for equality too, internal use only
|
|
851
|
+
:return: The lowest value of the source series
|
|
852
|
+
"""
|
|
853
|
+
last_min: Persistent[float] = na_float
|
|
854
|
+
last_min_index: Persistent[int] = 0
|
|
855
|
+
|
|
856
|
+
if last_min > source or (isinstance(last_min, NA) or last_min != last_min) or (_check_eq and last_min == source):
|
|
857
|
+
last_min = source
|
|
858
|
+
last_min_index = 0
|
|
859
|
+
|
|
860
|
+
if last_min_index >= length:
|
|
861
|
+
last_min = source
|
|
862
|
+
last_min_index = 0
|
|
863
|
+
for i in builtins.range(1, length):
|
|
864
|
+
s = source[i]
|
|
865
|
+
if s < last_min:
|
|
866
|
+
last_min = s
|
|
867
|
+
last_min_index = i
|
|
868
|
+
elif not _check_eq and s == last_min:
|
|
869
|
+
# For normal lowest: update index for equal values
|
|
870
|
+
last_min_index = i
|
|
871
|
+
# For pivot detection (_check_eq=True): don't update index for equal values
|
|
872
|
+
|
|
873
|
+
min_index = last_min_index
|
|
874
|
+
last_min_index += 1
|
|
875
|
+
|
|
876
|
+
if bar_index < length - 1:
|
|
877
|
+
return na_float if not _tuple else (na_float, NA(int)) # type: ignore[return-value]
|
|
878
|
+
|
|
879
|
+
if _bars:
|
|
880
|
+
return -min_index
|
|
881
|
+
if _tuple:
|
|
882
|
+
return last_min, -min_index # type: ignore[return-value]
|
|
883
|
+
return last_min
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@overload
|
|
887
|
+
def lowest(length: int) -> PyneFloat:
|
|
888
|
+
return lowest(low, length)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
# noinspection PyUnusedLocal
|
|
892
|
+
@overload
|
|
893
|
+
def lowestbars(source: Series[float], length: int) -> PyneFloat:
|
|
894
|
+
"""
|
|
895
|
+
Calculate the number of bars since the lowest value of the source series with the given length.
|
|
896
|
+
|
|
897
|
+
:param source: The source series
|
|
898
|
+
:param length: The length of the lowest value
|
|
899
|
+
:return: The number of bars since the lowest value of the source series
|
|
900
|
+
"""
|
|
901
|
+
return lowest(source, length, _bars=True)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
@overload
|
|
905
|
+
def lowestbars(length: int) -> PyneFloat:
|
|
906
|
+
return lowest(low, length, _bars=True)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def macd(source: float, fastlen: int, slowlen: int, siglen: int) \
|
|
910
|
+
-> tuple[PyneFloat, PyneFloat, PyneFloat]:
|
|
911
|
+
"""
|
|
912
|
+
Calculate the Moving Average Convergence Divergence (MACD) of the source series with the given
|
|
913
|
+
fast, slow, and signal lengths.
|
|
914
|
+
|
|
915
|
+
:param source: The source series
|
|
916
|
+
:param fastlen: The length of the fast EMA
|
|
917
|
+
:param slowlen: The length of the slow EMA
|
|
918
|
+
:param siglen: The length of the signal EMA
|
|
919
|
+
:return: Tuple of three MACD series:
|
|
920
|
+
- MACD Line
|
|
921
|
+
- Signal Line
|
|
922
|
+
- Histogram
|
|
923
|
+
"""
|
|
924
|
+
assert fastlen > 0, "Invalid fast length, fast length must be greater than 0!"
|
|
925
|
+
assert slowlen > 0, "Invalid slow length, slow length must be greater than 0!"
|
|
926
|
+
assert siglen > 0, "Invalid signal length, signal length must be greater than 0!"
|
|
927
|
+
fast = ema(source, fastlen)
|
|
928
|
+
slow = ema(source, slowlen)
|
|
929
|
+
if (isinstance(fast, NA) or fast != fast) or (isinstance(slow, NA) or slow != slow):
|
|
930
|
+
return na_float, na_float, na_float
|
|
931
|
+
macd_val = fast - slow
|
|
932
|
+
signal = ema(macd_val, siglen)
|
|
933
|
+
if (isinstance(signal, NA) or signal != signal):
|
|
934
|
+
return macd_val, na_float, na_float
|
|
935
|
+
return macd_val, signal, macd_val - signal
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
# noinspection PyShadowingBuiltins
|
|
939
|
+
def max(source: Series[float]) -> PyneFloat:
|
|
940
|
+
"""
|
|
941
|
+
Calculate the maximum value of the source series.
|
|
942
|
+
|
|
943
|
+
:param source: The source series
|
|
944
|
+
:return: The maximum value of the source series
|
|
945
|
+
"""
|
|
946
|
+
max_val: Persistent[float] = na_float
|
|
947
|
+
if max_val < source or (isinstance(max_val, NA) or max_val != max_val):
|
|
948
|
+
max_val = source
|
|
949
|
+
return max_val
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def median(source: Series[TFI], length: int) -> TFI:
|
|
953
|
+
"""
|
|
954
|
+
Calculate the median of the source series over a given period.
|
|
955
|
+
|
|
956
|
+
:param source: Input series of values
|
|
957
|
+
:param length: Number of bars to calculate over
|
|
958
|
+
:return: The median value or na during warmup
|
|
959
|
+
"""
|
|
960
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
961
|
+
if length == 1: # Shortcut
|
|
962
|
+
return source
|
|
963
|
+
length = int(length)
|
|
964
|
+
|
|
965
|
+
if (isinstance(source, NA) or source != source):
|
|
966
|
+
# type(source) would be the NA class itself — keep the source sentinel's type
|
|
967
|
+
return cast(TFI, source)
|
|
968
|
+
|
|
969
|
+
# Store heaps and window
|
|
970
|
+
heap_low: Persistent[list[TFI]] = [] # Max heap (negative values)
|
|
971
|
+
heap_high: Persistent[list[TFI]] = [] # Min heap
|
|
972
|
+
window: Persistent[list[TFI]] = [] # Recent values for removal
|
|
973
|
+
|
|
974
|
+
# Add new value and balance heaps
|
|
975
|
+
value = source
|
|
976
|
+
window.append(value)
|
|
977
|
+
heapq.heappush(heap_low, -value)
|
|
978
|
+
heapq.heappush(heap_high, -heapq.heappop(heap_low))
|
|
979
|
+
|
|
980
|
+
if len(heap_low) < len(heap_high):
|
|
981
|
+
heapq.heappush(heap_low, -heapq.heappop(heap_high))
|
|
982
|
+
|
|
983
|
+
# Remove old value if window full
|
|
984
|
+
if len(window) > length:
|
|
985
|
+
old = window.pop(0)
|
|
986
|
+
|
|
987
|
+
# Remove from correct heap
|
|
988
|
+
if old <= -heap_low[0]:
|
|
989
|
+
heap_low.remove(-old)
|
|
990
|
+
heapq.heapify(heap_low)
|
|
991
|
+
else:
|
|
992
|
+
heap_high.remove(old)
|
|
993
|
+
heapq.heapify(heap_high)
|
|
994
|
+
|
|
995
|
+
# Rebalance if needed
|
|
996
|
+
if len(heap_low) < len(heap_high):
|
|
997
|
+
heapq.heappush(heap_low, -heapq.heappop(heap_high))
|
|
998
|
+
elif len(heap_low) > len(heap_high) + 1:
|
|
999
|
+
heapq.heappush(heap_high, -heapq.heappop(heap_low))
|
|
1000
|
+
|
|
1001
|
+
# Return na during warmup
|
|
1002
|
+
if len(window) < length:
|
|
1003
|
+
return NA(cast(type[TFI], type(source))) # type: ignore
|
|
1004
|
+
|
|
1005
|
+
# Return median based on heap sizes
|
|
1006
|
+
if len(heap_low) > len(heap_high):
|
|
1007
|
+
return -heap_low[0] # Max heap root
|
|
1008
|
+
return -heap_low[0] if isinstance(source, int) else (-heap_low[0] + heap_high[0]) / 2 # type: ignore
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def mfi(source: float, length: int) -> PyneFloat:
|
|
1012
|
+
"""
|
|
1013
|
+
Calculate the Money Flow Index (MFI) of the source series with the given length.
|
|
1014
|
+
|
|
1015
|
+
:param source: The source series
|
|
1016
|
+
:param length: The length of the MFI
|
|
1017
|
+
:return: The Money Flow Index (MFI) of the source series
|
|
1018
|
+
"""
|
|
1019
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1020
|
+
if (isinstance(source, NA) or source != source):
|
|
1021
|
+
return na_float
|
|
1022
|
+
length = int(length)
|
|
1023
|
+
|
|
1024
|
+
chg = change(source)
|
|
1025
|
+
upper = lib_math.sum(volume * (0.0 if not (isinstance(chg, NA) or chg != chg) and chg <= 0 else source), length)
|
|
1026
|
+
lower = lib_math.sum(volume * (0.0 if not (isinstance(chg, NA) or chg != chg) and chg >= 0 else source), length)
|
|
1027
|
+
if (isinstance(upper, NA) or upper != upper) or (isinstance(lower, NA) or lower != lower):
|
|
1028
|
+
return na_float
|
|
1029
|
+
return 100.0 - (100 * lower / (upper + lower))
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
# noinspection PyShadowingBuiltins
|
|
1033
|
+
def min(source: Series[float]) -> PyneFloat:
|
|
1034
|
+
"""
|
|
1035
|
+
Calculate the minimum value of the source series.
|
|
1036
|
+
|
|
1037
|
+
:param source: The source series
|
|
1038
|
+
:return: The minimum value of the source series
|
|
1039
|
+
"""
|
|
1040
|
+
min_val: Persistent[float] = na_float
|
|
1041
|
+
if min_val > source or (isinstance(min_val, NA) or min_val != min_val):
|
|
1042
|
+
min_val = source
|
|
1043
|
+
return min_val
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def mode(source: Series[TFI], length: int) -> TFI:
|
|
1047
|
+
"""
|
|
1048
|
+
Returns the mode of the series. If there are several values with the same frequency,
|
|
1049
|
+
it returns the smallest value.
|
|
1050
|
+
|
|
1051
|
+
:param source: Series of values to process
|
|
1052
|
+
:param length: Number of bars (length)
|
|
1053
|
+
:return: The most frequently occurring value from the source. If none exists, returns
|
|
1054
|
+
the smallest value instead. Returns na during warm-up period.
|
|
1055
|
+
"""
|
|
1056
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1057
|
+
if (isinstance(source, NA) or source != source):
|
|
1058
|
+
return cast(TFI, source)
|
|
1059
|
+
if bar_index < length - 1:
|
|
1060
|
+
return cast(TFI, NA(builtins.type(source)))
|
|
1061
|
+
length = int(length)
|
|
1062
|
+
|
|
1063
|
+
# Store values for quick access
|
|
1064
|
+
values = [source[i] for i in builtins.range(length) if not (isinstance(source[i], NA) or source[i] != source[i])]
|
|
1065
|
+
if not values:
|
|
1066
|
+
return cast(TFI, NA(builtins.type(source)))
|
|
1067
|
+
|
|
1068
|
+
# Find mode - sort values to handle equal frequencies
|
|
1069
|
+
values.sort() # Ensure we pick the smallest value when frequencies are equal
|
|
1070
|
+
mode_val = values[0]
|
|
1071
|
+
current_val = values[0]
|
|
1072
|
+
max_freq = curr_freq = 1
|
|
1073
|
+
|
|
1074
|
+
# Single pass through sorted values
|
|
1075
|
+
for i in builtins.range(1, len(values)):
|
|
1076
|
+
if values[i] == current_val:
|
|
1077
|
+
curr_freq += 1
|
|
1078
|
+
if curr_freq > max_freq:
|
|
1079
|
+
max_freq = curr_freq
|
|
1080
|
+
mode_val = current_val
|
|
1081
|
+
else:
|
|
1082
|
+
current_val = values[i]
|
|
1083
|
+
curr_freq = 1
|
|
1084
|
+
|
|
1085
|
+
return mode_val
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def mom(source: float, length: int) -> PyneFloat:
|
|
1089
|
+
"""
|
|
1090
|
+
Calculate the Momentum of the source series with the given length.
|
|
1091
|
+
|
|
1092
|
+
:param source: The source series
|
|
1093
|
+
:param length: The length of the Momentum
|
|
1094
|
+
:return: The Momentum of the source series
|
|
1095
|
+
"""
|
|
1096
|
+
# It is exactly the same as change function
|
|
1097
|
+
return change(source, length)
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
# noinspection PyUnusedLocal
|
|
1101
|
+
@module_property
|
|
1102
|
+
def nvi() -> PyneFloat:
|
|
1103
|
+
"""
|
|
1104
|
+
Negative Volume Index.
|
|
1105
|
+
|
|
1106
|
+
:return: Negative Volume Index
|
|
1107
|
+
"""
|
|
1108
|
+
prev_close: Persistent[float] = 0.0
|
|
1109
|
+
prev_volume: Persistent[float] = 0.0
|
|
1110
|
+
prev_nvi: Persistent[float] = 1.0
|
|
1111
|
+
|
|
1112
|
+
if close == 0.0 or prev_close == 0.0:
|
|
1113
|
+
_nvi = prev_nvi
|
|
1114
|
+
else:
|
|
1115
|
+
_nvi = prev_nvi + ((close - prev_close) / prev_close) * prev_nvi if volume < prev_volume else prev_nvi
|
|
1116
|
+
|
|
1117
|
+
prev_close = close
|
|
1118
|
+
prev_volume = volume
|
|
1119
|
+
prev_nvi = _nvi
|
|
1120
|
+
|
|
1121
|
+
return _nvi
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
@module_property
|
|
1125
|
+
def obv() -> PyneFloat:
|
|
1126
|
+
"""
|
|
1127
|
+
On Balance Volume.
|
|
1128
|
+
|
|
1129
|
+
:return: On Balance Volume
|
|
1130
|
+
"""
|
|
1131
|
+
chg = change(close)
|
|
1132
|
+
if (isinstance(chg, NA) or chg != chg):
|
|
1133
|
+
return na_float
|
|
1134
|
+
if chg > 0:
|
|
1135
|
+
chg = 1.0
|
|
1136
|
+
elif chg < 0:
|
|
1137
|
+
chg = -1.0
|
|
1138
|
+
else:
|
|
1139
|
+
chg = 0.0
|
|
1140
|
+
return cum(volume * chg)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def percentile_linear_interpolation(source: Series[float], length: int, percentage: int | float) \
|
|
1144
|
+
-> PyneFloat:
|
|
1145
|
+
"""
|
|
1146
|
+
Calculates percentile using method of linear interpolation between the two nearest ranks.
|
|
1147
|
+
|
|
1148
|
+
:param source: The source series
|
|
1149
|
+
:param length: The length of the percentile
|
|
1150
|
+
:param percentage: The percentage of the percentile
|
|
1151
|
+
:return: The percentile of the source series
|
|
1152
|
+
"""
|
|
1153
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1154
|
+
length = int(length)
|
|
1155
|
+
# The final slice reads ``length`` candles of history; grow the source
|
|
1156
|
+
# buffer to fit it (the per-series default may be smaller). Done before the
|
|
1157
|
+
# warmup guard so the oldest candles are kept from the first bar on.
|
|
1158
|
+
max_bars_back(source, length)
|
|
1159
|
+
if (isinstance(source, NA) or source != source):
|
|
1160
|
+
return na_float
|
|
1161
|
+
|
|
1162
|
+
if bar_index < length - 1:
|
|
1163
|
+
return na_float
|
|
1164
|
+
|
|
1165
|
+
return array.percentile_linear_interpolation(source[:length], percentage) # type: ignore
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def percentile_nearest_rank(source: Series[float], length: int, percentage: int | float) \
|
|
1169
|
+
-> PyneFloat:
|
|
1170
|
+
"""
|
|
1171
|
+
Calculates percentile using the nearest rank method.
|
|
1172
|
+
|
|
1173
|
+
:param source: The source series
|
|
1174
|
+
:param length: The length of the percentile
|
|
1175
|
+
:param percentage: The percentage of the percentile
|
|
1176
|
+
:return: The percentile of the source series
|
|
1177
|
+
"""
|
|
1178
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1179
|
+
length = int(length)
|
|
1180
|
+
# The final slice reads ``length`` candles of history; grow the source
|
|
1181
|
+
# buffer to fit it (the per-series default may be smaller). Done before the
|
|
1182
|
+
# warmup guard so the oldest candles are kept from the first bar on.
|
|
1183
|
+
max_bars_back(source, length)
|
|
1184
|
+
if (isinstance(source, NA) or source != source):
|
|
1185
|
+
return na_float
|
|
1186
|
+
|
|
1187
|
+
if bar_index < length - 1:
|
|
1188
|
+
return na_float
|
|
1189
|
+
|
|
1190
|
+
return array.percentile_nearest_rank(source[:length], percentage) # type: ignore
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def percentrank(source: Series[float], length: int) -> PyneFloat:
|
|
1194
|
+
"""
|
|
1195
|
+
Percent rank is the percents of how many previous values was less than or equal to the current
|
|
1196
|
+
value of given series.
|
|
1197
|
+
|
|
1198
|
+
:param source: The source series
|
|
1199
|
+
:param length: Number of bars back to include in the calculation
|
|
1200
|
+
:return: The percentage of values less than or equal to the current value
|
|
1201
|
+
"""
|
|
1202
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1203
|
+
length = int(length)
|
|
1204
|
+
# The final slice reads ``length + 1`` candles of history; a buffer of
|
|
1205
|
+
# ``max_bars_back == length`` (capacity ``length + 1``) holds exactly that.
|
|
1206
|
+
# Done before the warmup guard so the oldest candles are kept from bar 0.
|
|
1207
|
+
max_bars_back(source, length)
|
|
1208
|
+
if (isinstance(source, NA) or source != source):
|
|
1209
|
+
return na_float
|
|
1210
|
+
|
|
1211
|
+
if bar_index < length:
|
|
1212
|
+
return na_float
|
|
1213
|
+
|
|
1214
|
+
return array.percentrank(source[:length + 1], 0) # type: ignore
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
# noinspection PyUnusedLocal,PyShadowingBuiltins
|
|
1218
|
+
def pivot_point_levels(type: str, anchor: bool, developing: bool = False) -> list[PyneFloat]:
|
|
1219
|
+
"""
|
|
1220
|
+
Calculate pivot point levels based on the specified calculation type.
|
|
1221
|
+
|
|
1222
|
+
Returns an array of 11 float values representing pivot point levels:
|
|
1223
|
+
[P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
|
|
1224
|
+
|
|
1225
|
+
:param type: Pivot calculation type: "Traditional", "Fibonacci", "Woodie",
|
|
1226
|
+
"Classic", "DM", or "Camarilla"
|
|
1227
|
+
:param anchor: Condition that triggers recalculation of pivot levels
|
|
1228
|
+
(e.g., timeframe.change("D"))
|
|
1229
|
+
:param developing: If true, values recalculate on each bar using current OHLC;
|
|
1230
|
+
if false (default), values remain constant until the next anchor
|
|
1231
|
+
:return: Array of 11 float values: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
|
|
1232
|
+
Not all types support all levels - unsupported ones return na_float
|
|
1233
|
+
"""
|
|
1234
|
+
# Persistent state for anchor-based calculation
|
|
1235
|
+
# These store the COMPLETED previous period's values (used when developing=False)
|
|
1236
|
+
prev_period_high: Persistent[float] = na_float
|
|
1237
|
+
prev_period_low: Persistent[float] = na_float
|
|
1238
|
+
prev_period_close: Persistent[float] = na_float
|
|
1239
|
+
prev_period_open: Persistent[float] = na_float
|
|
1240
|
+
|
|
1241
|
+
# These accumulate values for the CURRENT period (will become prev_period on next anchor)
|
|
1242
|
+
curr_period_high: Persistent[float] = na_float
|
|
1243
|
+
curr_period_low: Persistent[float] = na_float
|
|
1244
|
+
curr_period_open: Persistent[float] = na_float
|
|
1245
|
+
is_first_bar_of_period: Persistent[bool] = True
|
|
1246
|
+
|
|
1247
|
+
levels: Persistent[list[PyneFloat]] = [na_float] * 11
|
|
1248
|
+
had_anchor: Persistent[bool] = False
|
|
1249
|
+
|
|
1250
|
+
# Normalize type to lowercase for case-insensitive comparison
|
|
1251
|
+
type_lower = type.lower() if isinstance(type, str) else ""
|
|
1252
|
+
|
|
1253
|
+
# On anchor, finalize the previous period and start a new one
|
|
1254
|
+
if anchor:
|
|
1255
|
+
# Save the accumulated current period values as the previous period
|
|
1256
|
+
prev_period_high = curr_period_high
|
|
1257
|
+
prev_period_low = curr_period_low
|
|
1258
|
+
prev_period_close = close[1] if bar_index > 0 else close # Last close of prev period
|
|
1259
|
+
prev_period_open = curr_period_open
|
|
1260
|
+
|
|
1261
|
+
# Reset current period accumulators for the new period
|
|
1262
|
+
curr_period_high = high
|
|
1263
|
+
curr_period_low = low
|
|
1264
|
+
curr_period_open = open
|
|
1265
|
+
is_first_bar_of_period = False
|
|
1266
|
+
had_anchor = True
|
|
1267
|
+
else:
|
|
1268
|
+
# Accumulate OHLC extremes for current period
|
|
1269
|
+
if is_first_bar_of_period or (isinstance(curr_period_high, NA) or curr_period_high != curr_period_high):
|
|
1270
|
+
curr_period_high = high
|
|
1271
|
+
curr_period_low = low
|
|
1272
|
+
curr_period_open = open
|
|
1273
|
+
is_first_bar_of_period = False
|
|
1274
|
+
else:
|
|
1275
|
+
if not (isinstance(high, NA) or high != high) and high > curr_period_high:
|
|
1276
|
+
curr_period_high = high
|
|
1277
|
+
if not (isinstance(low, NA) or low != low) and low < curr_period_low:
|
|
1278
|
+
curr_period_low = low
|
|
1279
|
+
|
|
1280
|
+
# If no anchor has occurred yet, return all NA values
|
|
1281
|
+
if not had_anchor:
|
|
1282
|
+
return [na_float] * 11
|
|
1283
|
+
|
|
1284
|
+
# Determine which OHLC values to use
|
|
1285
|
+
if developing:
|
|
1286
|
+
# Use current accumulated values for developing mode
|
|
1287
|
+
h = curr_period_high
|
|
1288
|
+
l = curr_period_low
|
|
1289
|
+
c = close # Current close for developing
|
|
1290
|
+
o = curr_period_open
|
|
1291
|
+
else:
|
|
1292
|
+
# Use previous period's OHLC (fixed after anchor)
|
|
1293
|
+
h = prev_period_high
|
|
1294
|
+
l = prev_period_low
|
|
1295
|
+
c = prev_period_close
|
|
1296
|
+
o = prev_period_open
|
|
1297
|
+
|
|
1298
|
+
# Check for NA values
|
|
1299
|
+
if (isinstance(h, NA) or h != h) or (isinstance(l, NA) or l != l) or (isinstance(c, NA) or c != c):
|
|
1300
|
+
return [na_float] * 11
|
|
1301
|
+
|
|
1302
|
+
# Calculate range
|
|
1303
|
+
rng = h - l
|
|
1304
|
+
|
|
1305
|
+
# Calculate levels based on type
|
|
1306
|
+
if type_lower in ("traditional", "classic"):
|
|
1307
|
+
# Traditional/Classic Pivot Points
|
|
1308
|
+
p = (h + l + c) / 3
|
|
1309
|
+
r1 = 2 * p - l
|
|
1310
|
+
s1 = 2 * p - h
|
|
1311
|
+
r2 = p + rng
|
|
1312
|
+
s2 = p - rng
|
|
1313
|
+
r3 = r1 + rng
|
|
1314
|
+
s3 = s1 - rng
|
|
1315
|
+
levels = [p, r1, s1, r2, s2, r3, s3, na_float, na_float, na_float, na_float]
|
|
1316
|
+
|
|
1317
|
+
elif type_lower == "fibonacci":
|
|
1318
|
+
# Fibonacci Pivot Points
|
|
1319
|
+
p = (h + l + c) / 3
|
|
1320
|
+
r1 = p + 0.382 * rng
|
|
1321
|
+
s1 = p - 0.382 * rng
|
|
1322
|
+
r2 = p + 0.618 * rng
|
|
1323
|
+
s2 = p - 0.618 * rng
|
|
1324
|
+
r3 = p + 1.000 * rng
|
|
1325
|
+
s3 = p - 1.000 * rng
|
|
1326
|
+
levels = [p, r1, s1, r2, s2, r3, s3, na_float, na_float, na_float, na_float]
|
|
1327
|
+
|
|
1328
|
+
elif type_lower == "woodie":
|
|
1329
|
+
# Woodie Pivot Points
|
|
1330
|
+
# Note: Woodie uses current period's OPEN (not prev period's close) for the "close" component
|
|
1331
|
+
# This makes Woodie more responsive to current price action
|
|
1332
|
+
woodie_c = curr_period_open if not developing else close
|
|
1333
|
+
if (isinstance(woodie_c, NA) or woodie_c != woodie_c):
|
|
1334
|
+
return [na_float] * 11
|
|
1335
|
+
p = (h + l + 2 * woodie_c) / 4
|
|
1336
|
+
r1 = 2 * p - l
|
|
1337
|
+
s1 = 2 * p - h
|
|
1338
|
+
r2 = p + rng
|
|
1339
|
+
s2 = p - rng
|
|
1340
|
+
r3 = r1 + rng
|
|
1341
|
+
s3 = s1 - rng
|
|
1342
|
+
levels = [p, r1, s1, r2, s2, r3, s3, na_float, na_float, na_float, na_float]
|
|
1343
|
+
|
|
1344
|
+
elif type_lower == "dm":
|
|
1345
|
+
# DeMark Pivot Points
|
|
1346
|
+
if (isinstance(o, NA) or o != o):
|
|
1347
|
+
return [na_float] * 11
|
|
1348
|
+
|
|
1349
|
+
if c < o:
|
|
1350
|
+
x = h + 2 * l + c
|
|
1351
|
+
elif c > o:
|
|
1352
|
+
x = 2 * h + l + c
|
|
1353
|
+
else: # c == o
|
|
1354
|
+
x = h + l + 2 * c
|
|
1355
|
+
|
|
1356
|
+
p = x / 4
|
|
1357
|
+
r1 = x / 2 - l
|
|
1358
|
+
s1 = x / 2 - h
|
|
1359
|
+
# DM only has P, R1, S1
|
|
1360
|
+
levels = [p, r1, s1, na_float, na_float, na_float, na_float,
|
|
1361
|
+
na_float, na_float, na_float, na_float]
|
|
1362
|
+
|
|
1363
|
+
elif type_lower == "camarilla":
|
|
1364
|
+
# Camarilla Pivot Points
|
|
1365
|
+
p = (h + l + c) / 3
|
|
1366
|
+
r1 = c + rng * 1.1 / 12
|
|
1367
|
+
s1 = c - rng * 1.1 / 12
|
|
1368
|
+
r2 = c + rng * 1.1 / 6
|
|
1369
|
+
s2 = c - rng * 1.1 / 6
|
|
1370
|
+
r3 = c + rng * 1.1 / 4
|
|
1371
|
+
s3 = c - rng * 1.1 / 4
|
|
1372
|
+
r4 = c + rng * 1.1 / 2
|
|
1373
|
+
s4 = c - rng * 1.1 / 2
|
|
1374
|
+
# Camarilla has P, R1-R4, S1-S4 (no R5, S5)
|
|
1375
|
+
levels = [p, r1, s1, r2, s2, r3, s3, r4, s4, na_float, na_float]
|
|
1376
|
+
|
|
1377
|
+
else:
|
|
1378
|
+
# Unknown type - return all NA
|
|
1379
|
+
levels = [na_float] * 11
|
|
1380
|
+
|
|
1381
|
+
return levels
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
@overload
|
|
1385
|
+
def pivothigh(source: float, leftbars: int, rightbars: int) -> PyneFloat:
|
|
1386
|
+
"""
|
|
1387
|
+
This function returns price of the pivot high point. It returns 'NaN', if there was no pivot high point.
|
|
1388
|
+
|
|
1389
|
+
:param source: The source series
|
|
1390
|
+
:param leftbars: Left strength
|
|
1391
|
+
:param rightbars: Right strength.
|
|
1392
|
+
:return: Price of the pivot high point, or NaN if no pivot
|
|
1393
|
+
"""
|
|
1394
|
+
assert leftbars > 0, "Invalid leftbars, leftbars must be greater than 0!"
|
|
1395
|
+
assert rightbars > 0, "Invalid rightbars, rightbars must be greater than 0!"
|
|
1396
|
+
|
|
1397
|
+
if (isinstance(source, NA) or source != source):
|
|
1398
|
+
return na_float
|
|
1399
|
+
|
|
1400
|
+
pivotrange = leftbars + rightbars + 1
|
|
1401
|
+
ph, pi = cast(tuple[float, int], highest(source, pivotrange, _tuple=True, _check_eq=True))
|
|
1402
|
+
|
|
1403
|
+
if pi == -rightbars:
|
|
1404
|
+
return ph
|
|
1405
|
+
|
|
1406
|
+
return na_float
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
@overload
|
|
1410
|
+
def pivothigh(leftbars: int, rightbars: int) -> PyneFloat:
|
|
1411
|
+
"""
|
|
1412
|
+
This function returns price of the pivot high point. It returns 'NaN', if there was no pivot high point.
|
|
1413
|
+
|
|
1414
|
+
:param leftbars: Left strength
|
|
1415
|
+
:param rightbars: Right strength.
|
|
1416
|
+
:return: Price of the pivot high point, or NaN if no pivot
|
|
1417
|
+
"""
|
|
1418
|
+
try:
|
|
1419
|
+
return pivothigh(safe_convert.safe_float(high), leftbars, rightbars) # type: ignore
|
|
1420
|
+
except TypeError:
|
|
1421
|
+
if (isinstance(high, NA) or high != high):
|
|
1422
|
+
return na_float
|
|
1423
|
+
else:
|
|
1424
|
+
raise
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
@overload
|
|
1428
|
+
def pivotlow(source: float, leftbars: int, rightbars: int) -> PyneFloat:
|
|
1429
|
+
"""
|
|
1430
|
+
This function returns price of the pivot low point. It returns 'NaN', if there was no pivot low point.
|
|
1431
|
+
|
|
1432
|
+
:param source: The source series
|
|
1433
|
+
:param leftbars: Left strength
|
|
1434
|
+
:param rightbars: Right strength.
|
|
1435
|
+
:return: Price of the pivot low point, or NaN if no pivot
|
|
1436
|
+
"""
|
|
1437
|
+
assert leftbars > 0, "Invalid leftbars, leftbars must be greater than 0!"
|
|
1438
|
+
assert rightbars > 0, "Invalid rightbars, rightbars must be greater than 0!"
|
|
1439
|
+
|
|
1440
|
+
if (isinstance(source, NA) or source != source):
|
|
1441
|
+
return na_float
|
|
1442
|
+
|
|
1443
|
+
pivotrange = leftbars + rightbars + 1
|
|
1444
|
+
pl, pi = cast(tuple[float, int], lowest(source, pivotrange, _tuple=True, _check_eq=True))
|
|
1445
|
+
if pi == -rightbars:
|
|
1446
|
+
return pl
|
|
1447
|
+
|
|
1448
|
+
return na_float
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
@overload
|
|
1452
|
+
def pivotlow(leftbars: int, rightbars: int) -> PyneFloat:
|
|
1453
|
+
"""
|
|
1454
|
+
This function returns price of the pivot low point. It returns 'NaN', if there was no pivot low point.
|
|
1455
|
+
|
|
1456
|
+
:param leftbars: Left strength
|
|
1457
|
+
:param rightbars: Right strength.
|
|
1458
|
+
:return: Price of the pivot low point, or NaN if no pivot
|
|
1459
|
+
"""
|
|
1460
|
+
try:
|
|
1461
|
+
return pivotlow(safe_convert.safe_float(low), leftbars, rightbars) # type: ignore
|
|
1462
|
+
except TypeError:
|
|
1463
|
+
if (isinstance(low, NA) or low != low):
|
|
1464
|
+
return na_float
|
|
1465
|
+
else:
|
|
1466
|
+
raise
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
# noinspection PyUnusedLocal
|
|
1470
|
+
@module_property
|
|
1471
|
+
def pvi() -> PyneFloat:
|
|
1472
|
+
"""
|
|
1473
|
+
Positive Volume Index.
|
|
1474
|
+
|
|
1475
|
+
:return: Positive Volume Index
|
|
1476
|
+
"""
|
|
1477
|
+
prev_close: Persistent[float] = 0.0
|
|
1478
|
+
prev_volume: Persistent[float] = 0.0
|
|
1479
|
+
prev_pvi: Persistent[float] = 1.0
|
|
1480
|
+
|
|
1481
|
+
_pvi = prev_pvi + ((close - prev_close) / prev_close) * prev_pvi if volume > prev_volume else prev_pvi
|
|
1482
|
+
# na() predicate semantics: a division by zero (prev_close warmup 0.0) gives
|
|
1483
|
+
# inf, which is na on TV — not just nan, so the guard must be isfinite-based.
|
|
1484
|
+
if isinstance(_pvi, NA) or not math.isfinite(_pvi):
|
|
1485
|
+
_pvi = prev_pvi
|
|
1486
|
+
|
|
1487
|
+
prev_close = close
|
|
1488
|
+
prev_volume = volume
|
|
1489
|
+
prev_pvi = _pvi
|
|
1490
|
+
|
|
1491
|
+
return _pvi
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
# noinspection PyUnusedLocal
|
|
1495
|
+
@module_property
|
|
1496
|
+
def pvt() -> PyneFloat:
|
|
1497
|
+
"""
|
|
1498
|
+
Price Volume Trend.
|
|
1499
|
+
|
|
1500
|
+
:return: Price Volume Trend
|
|
1501
|
+
"""
|
|
1502
|
+
prev_close: Persistent[float] = na_float
|
|
1503
|
+
chg = close - prev_close
|
|
1504
|
+
res = cum((chg / prev_close) * volume)
|
|
1505
|
+
prev_close = close
|
|
1506
|
+
return res
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
# noinspection PyShadowingBuiltins
|
|
1510
|
+
def range(source: Series[float], length: int) -> PyneFloat:
|
|
1511
|
+
"""
|
|
1512
|
+
Returns the difference between the max and min values in a series.
|
|
1513
|
+
|
|
1514
|
+
:param source: The source series
|
|
1515
|
+
:param length: Number of bars
|
|
1516
|
+
:return: The range of the source series
|
|
1517
|
+
"""
|
|
1518
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1519
|
+
if (isinstance(source, NA) or source != source):
|
|
1520
|
+
return na_float
|
|
1521
|
+
length = int(length)
|
|
1522
|
+
|
|
1523
|
+
return highest(source, length) - lowest(source, length)
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def rci(source: Series[float], length: int) -> PyneFloat:
|
|
1527
|
+
"""
|
|
1528
|
+
Calculate Rank Correlation Index (RCI).
|
|
1529
|
+
|
|
1530
|
+
:param source: Series of values to calculate RCI for
|
|
1531
|
+
:param length: Length of RCI calculation period
|
|
1532
|
+
:return: RCI value between -100 and 100, or na during warmup
|
|
1533
|
+
"""
|
|
1534
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1535
|
+
length = int(length)
|
|
1536
|
+
# The slice below reads ``length`` candles of history; grow the source
|
|
1537
|
+
# buffer to fit it (the per-series default may be smaller). Done before the
|
|
1538
|
+
# warmup guard so the oldest candles are kept from the first bar on.
|
|
1539
|
+
max_bars_back(source, length)
|
|
1540
|
+
if (isinstance(source, NA) or source != source):
|
|
1541
|
+
return na_float
|
|
1542
|
+
|
|
1543
|
+
if bar_index < length:
|
|
1544
|
+
return na_float
|
|
1545
|
+
|
|
1546
|
+
# Collect values for performance
|
|
1547
|
+
values = cast(list[float], source[:length]) # type: ignore
|
|
1548
|
+
|
|
1549
|
+
# Calculate sums for correlation
|
|
1550
|
+
sum_x = sum_y = sum_xy = sum_x2 = sum_y2 = 0.0
|
|
1551
|
+
|
|
1552
|
+
for i in builtins.range(length):
|
|
1553
|
+
x = i + 1 # Time rank (newest value gets highest rank)
|
|
1554
|
+
y = _avgrank(values, values[i]) # Data rank # type: ignore
|
|
1555
|
+
|
|
1556
|
+
sum_x += x
|
|
1557
|
+
sum_y += y
|
|
1558
|
+
sum_xy += x * y
|
|
1559
|
+
sum_x2 += x * x
|
|
1560
|
+
sum_y2 += y * y
|
|
1561
|
+
|
|
1562
|
+
# Calculate correlation coefficient
|
|
1563
|
+
n = length
|
|
1564
|
+
numerator = n * sum_xy - sum_x * sum_y
|
|
1565
|
+
denominator = math.sqrt((n * sum_x2 - sum_x * sum_x) * (n * sum_y2 - sum_y * sum_y))
|
|
1566
|
+
# Divide first, then scale by 100. TradingView computes rci in this exact
|
|
1567
|
+
# (num / den) * 100 order, floating-point rounding and all, so this form
|
|
1568
|
+
# reproduces its output. The mathematically "correctly rounded" alternative
|
|
1569
|
+
# num * 100 / den lands 1 ULP off at near-tie bars and diverges from TV: on
|
|
1570
|
+
# the wild RCI-strategy reference (BINANCE:BTCUSDT 30m) it raised the
|
|
1571
|
+
# extra/missing entry divergence from 14/6 to 30/11. Verified — do not reorder.
|
|
1572
|
+
return (numerator / denominator) * 100
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
# noinspection PyUnusedLocal
|
|
1576
|
+
def rising(source: float, length: int) -> bool:
|
|
1577
|
+
"""
|
|
1578
|
+
Test if the source series is now rising for length bars long.
|
|
1579
|
+
|
|
1580
|
+
:param source: The source series
|
|
1581
|
+
:param length: The length of the rising test
|
|
1582
|
+
:return: True if the source series is rising for length bars long
|
|
1583
|
+
"""
|
|
1584
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1585
|
+
length = int(length)
|
|
1586
|
+
|
|
1587
|
+
last_val: Persistent[float] = na_float
|
|
1588
|
+
counter: Persistent[int] = 0
|
|
1589
|
+
|
|
1590
|
+
if (isinstance(last_val, NA) or last_val != last_val):
|
|
1591
|
+
last_val = source
|
|
1592
|
+
return False
|
|
1593
|
+
|
|
1594
|
+
if source > last_val:
|
|
1595
|
+
counter += 1
|
|
1596
|
+
else:
|
|
1597
|
+
counter = 0
|
|
1598
|
+
|
|
1599
|
+
last_val = source
|
|
1600
|
+
return counter >= length
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def rma(source: PyneFloat, length: int) -> PyneFloat:
|
|
1604
|
+
"""
|
|
1605
|
+
Calculate the RMA (Running Moving Average) of the source series with the given length.
|
|
1606
|
+
|
|
1607
|
+
:param source: The source series
|
|
1608
|
+
:param length: The length of the RMA
|
|
1609
|
+
:return: The RMA of the source series
|
|
1610
|
+
"""
|
|
1611
|
+
return ema(source, length, 1 / length)
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
def roc(source: Series[float], length: int) -> PyneFloat:
|
|
1615
|
+
"""
|
|
1616
|
+
Calculate the Rate of Change (ROC) of the source series with the given length.
|
|
1617
|
+
|
|
1618
|
+
:param source: The source series
|
|
1619
|
+
:param length: The length of the ROC
|
|
1620
|
+
:return: The Rate of Change (ROC) of the source series
|
|
1621
|
+
"""
|
|
1622
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1623
|
+
if (isinstance(source, NA) or source != source):
|
|
1624
|
+
return na_float
|
|
1625
|
+
length = int(length)
|
|
1626
|
+
# Grow the buffer so ``source[length]`` stays addressable for lengths beyond the
|
|
1627
|
+
# per-series default max_bars_back (500); otherwise it reads na and the roc is na.
|
|
1628
|
+
max_bars_back(source, length)
|
|
1629
|
+
|
|
1630
|
+
prev_val = source[length]
|
|
1631
|
+
chg = change(source, length)
|
|
1632
|
+
|
|
1633
|
+
if (isinstance(prev_val, NA) or prev_val != prev_val):
|
|
1634
|
+
return na_float
|
|
1635
|
+
|
|
1636
|
+
return 100 * chg / prev_val
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
# noinspection PyUnusedLocal
|
|
1640
|
+
def rsi(source: float, length: int) -> PyneFloat:
|
|
1641
|
+
"""
|
|
1642
|
+
Calculate the Relative Strength Index (RSI) of the source series with the given length.
|
|
1643
|
+
|
|
1644
|
+
:param source: The source series
|
|
1645
|
+
:param length: The length of the RSI
|
|
1646
|
+
:return: The Relative Strength Index (RSI) of the source series
|
|
1647
|
+
"""
|
|
1648
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1649
|
+
if (isinstance(source, NA) or source != source):
|
|
1650
|
+
return na_float
|
|
1651
|
+
|
|
1652
|
+
prev_src: Persistent[float] = na_float
|
|
1653
|
+
if (isinstance(prev_src, NA) or prev_src != prev_src):
|
|
1654
|
+
prev_src = source
|
|
1655
|
+
return na_float
|
|
1656
|
+
|
|
1657
|
+
rma_u = rma(builtins.max(source - prev_src, 0.0), length)
|
|
1658
|
+
rma_d = rma(builtins.max(prev_src - source, 0.0), length)
|
|
1659
|
+
prev_src = source
|
|
1660
|
+
|
|
1661
|
+
return 100 - 100 / (1 + rma_u / rma_d)
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
# noinspection PyShadowingBuiltins,PyUnusedLocal,PyShadowingNames
|
|
1665
|
+
def sar(start: float = 0.02, inc: float = 0.02, max: float = 0.2) -> PyneFloat:
|
|
1666
|
+
"""
|
|
1667
|
+
Parabolic SAR (Stop and Reverse) - method devised by J. Welles Wilder, Jr.,
|
|
1668
|
+
to find potential reversals in the market price direction of traded goods.
|
|
1669
|
+
|
|
1670
|
+
:param start: Starting value for acceleration factor
|
|
1671
|
+
:param inc: Acceleration factor increment
|
|
1672
|
+
:param max: Maximum acceleration factor value
|
|
1673
|
+
:return: SAR value for current bar
|
|
1674
|
+
"""
|
|
1675
|
+
assert 0 < start <= max, "Start must be positive and not greater than max!"
|
|
1676
|
+
assert inc > 0, "Increment must be positive!"
|
|
1677
|
+
assert max <= 0.5, "Maximum cannot exceed 0.5!"
|
|
1678
|
+
|
|
1679
|
+
if bar_index == 0:
|
|
1680
|
+
return na_float
|
|
1681
|
+
|
|
1682
|
+
# Persistent states
|
|
1683
|
+
pos_long: Persistent[bool] = True # Current position (long/short)
|
|
1684
|
+
af: Persistent[float] = start # Current acceleration factor
|
|
1685
|
+
sar_val: Persistent[float] = na_float # Current SAR value
|
|
1686
|
+
ep: Persistent[float] = na_float # Extreme point
|
|
1687
|
+
|
|
1688
|
+
# Initialize on second bar
|
|
1689
|
+
if bar_index == 1:
|
|
1690
|
+
if high[1] > high:
|
|
1691
|
+
pos_long = False
|
|
1692
|
+
sar_val = high[1] # short start
|
|
1693
|
+
ep = low # EP is current low
|
|
1694
|
+
else:
|
|
1695
|
+
pos_long = True
|
|
1696
|
+
sar_val = low[1] # long start
|
|
1697
|
+
ep = high # EP is current high
|
|
1698
|
+
return sar_val
|
|
1699
|
+
|
|
1700
|
+
# Calculate next SAR value
|
|
1701
|
+
next_sar = sar_val + af * (ep - sar_val)
|
|
1702
|
+
|
|
1703
|
+
# Trend-dependent logic
|
|
1704
|
+
if pos_long:
|
|
1705
|
+
# Long trend
|
|
1706
|
+
if low <= next_sar: # Reverse to short
|
|
1707
|
+
pos_long = False
|
|
1708
|
+
af = start
|
|
1709
|
+
next_sar = ep # Start from previous EP (Wilder method)
|
|
1710
|
+
# Clip to current and previous 2 candle highs
|
|
1711
|
+
next_sar = builtins.max(
|
|
1712
|
+
next_sar,
|
|
1713
|
+
high,
|
|
1714
|
+
high[1],
|
|
1715
|
+
high[2] if not (isinstance(high[2], NA) or high[2] != high[2]) else high[1]
|
|
1716
|
+
)
|
|
1717
|
+
ep = low # New EP
|
|
1718
|
+
else:
|
|
1719
|
+
# Continue long
|
|
1720
|
+
next_sar = builtins.min(
|
|
1721
|
+
next_sar,
|
|
1722
|
+
low[1],
|
|
1723
|
+
low[2] if not (isinstance(low[2], NA) or low[2] != low[2]) else low[1]
|
|
1724
|
+
)
|
|
1725
|
+
if high > ep: # New peak
|
|
1726
|
+
ep = high
|
|
1727
|
+
af = builtins.min(af + inc, max)
|
|
1728
|
+
else:
|
|
1729
|
+
# Short trend
|
|
1730
|
+
if high >= next_sar: # Reverse to long
|
|
1731
|
+
pos_long = True
|
|
1732
|
+
af = start
|
|
1733
|
+
next_sar = ep # Start from previous EP (Wilder method)
|
|
1734
|
+
# Clip to current and previous 2 candle lows
|
|
1735
|
+
next_sar = builtins.min(
|
|
1736
|
+
next_sar,
|
|
1737
|
+
low,
|
|
1738
|
+
low[1],
|
|
1739
|
+
low[2] if not (isinstance(low[2], NA) or low[2] != low[2]) else low[1]
|
|
1740
|
+
)
|
|
1741
|
+
ep = high # New EP
|
|
1742
|
+
else:
|
|
1743
|
+
# Continue short
|
|
1744
|
+
next_sar = builtins.max(
|
|
1745
|
+
next_sar,
|
|
1746
|
+
high[1],
|
|
1747
|
+
high[2] if not (isinstance(high[2], NA) or high[2] != high[2]) else high[1]
|
|
1748
|
+
)
|
|
1749
|
+
if low < ep: # New trough
|
|
1750
|
+
ep = low
|
|
1751
|
+
af = builtins.min(af + inc, max)
|
|
1752
|
+
|
|
1753
|
+
sar_val = next_sar
|
|
1754
|
+
return sar_val
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def sma(source: Series[float], length: int) -> PyneFloat:
|
|
1758
|
+
"""
|
|
1759
|
+
Calculate Simple Moving Average (SMA)
|
|
1760
|
+
|
|
1761
|
+
:param source: The source series
|
|
1762
|
+
:param length: The length of the moving average
|
|
1763
|
+
:return: The Simple Moving Average (SMA)
|
|
1764
|
+
"""
|
|
1765
|
+
# Round is necessary to solve precision issues
|
|
1766
|
+
return round(lib_math.sum(source, length) / length, 15)
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
def stdev(source: float, length: int, biased=True) -> PyneFloat:
|
|
1770
|
+
"""
|
|
1771
|
+
Calculate the standard deviation of the source series with the given length.
|
|
1772
|
+
|
|
1773
|
+
:param source: The source series
|
|
1774
|
+
:param length: The length of the standard deviation
|
|
1775
|
+
:param biased: Specifies whether the biased or unbiased standard deviation is calculated
|
|
1776
|
+
:return: The standard deviation of the source series
|
|
1777
|
+
"""
|
|
1778
|
+
try:
|
|
1779
|
+
return math.sqrt(variance(source, length, biased))
|
|
1780
|
+
except TypeError:
|
|
1781
|
+
return na_float
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
# noinspection PyShadowingNames
|
|
1785
|
+
def stoch(source: float | Series[float], high: float | Series[float], low: float | Series[float],
|
|
1786
|
+
length: int) -> PyneFloat:
|
|
1787
|
+
"""
|
|
1788
|
+
Calculate the Stochastic Oscillator of the source series with the given length.
|
|
1789
|
+
|
|
1790
|
+
:param source: The source series
|
|
1791
|
+
:param high: Series of high values
|
|
1792
|
+
:param low: Series of low values
|
|
1793
|
+
:param length: The length of the Stochastic Oscillator
|
|
1794
|
+
:return: The Stochastic Oscillator of the source series
|
|
1795
|
+
"""
|
|
1796
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
1797
|
+
if (isinstance(source, NA) or source != source) or (isinstance(high, NA) or high != high) or (isinstance(low, NA) or low != low):
|
|
1798
|
+
return na_float
|
|
1799
|
+
length = int(length)
|
|
1800
|
+
|
|
1801
|
+
highs: Series[float] = high
|
|
1802
|
+
lows: Series[float] = low
|
|
1803
|
+
hmax = highest(highs, length)
|
|
1804
|
+
lmin = lowest(lows, length)
|
|
1805
|
+
|
|
1806
|
+
if bar_index < length - 1:
|
|
1807
|
+
return na_float
|
|
1808
|
+
|
|
1809
|
+
dl_diff = source - lmin
|
|
1810
|
+
hl_diff = hmax - lmin
|
|
1811
|
+
if dl_diff < 0.0:
|
|
1812
|
+
k = 0.0
|
|
1813
|
+
else:
|
|
1814
|
+
k = 100 * dl_diff / hl_diff
|
|
1815
|
+
k = 100.0 if k > 100.0 else 0.0 if k < 0.0 else k
|
|
1816
|
+
return k # type: ignore
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
# noinspection PyUnusedLocal,PyShadowingNames
|
|
1820
|
+
def supertrend(factor: float | int, atr_period: int) -> tuple[PyneFloat, PyneInt]:
|
|
1821
|
+
"""
|
|
1822
|
+
Calculate Supertrend indicator.
|
|
1823
|
+
|
|
1824
|
+
:param factor: ATR multiplier
|
|
1825
|
+
:param atr_period: ATR period length
|
|
1826
|
+
:return: Tuple of (supertrend value, direction). Direction: 1=down, -1=up
|
|
1827
|
+
"""
|
|
1828
|
+
assert atr_period > 0, "Invalid ATR period, must be greater than 0!"
|
|
1829
|
+
|
|
1830
|
+
# Store persistent state
|
|
1831
|
+
prev_lower: Persistent[float] = na_float
|
|
1832
|
+
prev_upper: Persistent[float] = na_float
|
|
1833
|
+
prev_close: Persistent[float] = na_float
|
|
1834
|
+
prev_direction: Persistent[int] = NA(int)
|
|
1835
|
+
prev_supertrend: Persistent[float] = na_float
|
|
1836
|
+
|
|
1837
|
+
# Calculate base values
|
|
1838
|
+
src = hl2
|
|
1839
|
+
atr_val = atr(atr_period)
|
|
1840
|
+
|
|
1841
|
+
# This is a strange bug in Pine Script, but we need to replicate it
|
|
1842
|
+
if bar_index == 0:
|
|
1843
|
+
return 0.0, 1
|
|
1844
|
+
|
|
1845
|
+
if (isinstance(src, NA) or src != src) or (isinstance(atr_val, NA) or atr_val != atr_val):
|
|
1846
|
+
return na_float, prev_direction if not (isinstance(prev_direction, NA) or prev_direction != prev_direction) else 1
|
|
1847
|
+
|
|
1848
|
+
# Calculate bands
|
|
1849
|
+
upper = src + factor * atr_val
|
|
1850
|
+
lower = src - factor * atr_val
|
|
1851
|
+
|
|
1852
|
+
# First value initialization
|
|
1853
|
+
if (isinstance(prev_direction, NA) or prev_direction != prev_direction):
|
|
1854
|
+
direction = 1
|
|
1855
|
+
supertrend = upper
|
|
1856
|
+
prev_direction = direction
|
|
1857
|
+
prev_supertrend = supertrend
|
|
1858
|
+
prev_lower = lower
|
|
1859
|
+
prev_upper = upper
|
|
1860
|
+
prev_close = close
|
|
1861
|
+
return supertrend, direction
|
|
1862
|
+
|
|
1863
|
+
# Adjust bands based on previous values
|
|
1864
|
+
if lower > prev_lower or prev_close < prev_lower:
|
|
1865
|
+
curr_lower = lower
|
|
1866
|
+
else:
|
|
1867
|
+
curr_lower = prev_lower
|
|
1868
|
+
|
|
1869
|
+
if upper < prev_upper or prev_close > prev_upper:
|
|
1870
|
+
curr_upper = upper
|
|
1871
|
+
else:
|
|
1872
|
+
curr_upper = prev_upper
|
|
1873
|
+
|
|
1874
|
+
# Calculate direction
|
|
1875
|
+
if prev_supertrend == prev_upper:
|
|
1876
|
+
direction = -1 if close > curr_upper else 1
|
|
1877
|
+
else:
|
|
1878
|
+
direction = 1 if close < curr_lower else -1
|
|
1879
|
+
|
|
1880
|
+
# Calculate supertrend value
|
|
1881
|
+
supertrend = curr_upper if direction == 1 else curr_lower
|
|
1882
|
+
|
|
1883
|
+
# Store values for next iteration
|
|
1884
|
+
prev_direction = direction
|
|
1885
|
+
prev_supertrend = supertrend
|
|
1886
|
+
prev_lower = curr_lower
|
|
1887
|
+
prev_upper = curr_upper
|
|
1888
|
+
prev_close = close
|
|
1889
|
+
|
|
1890
|
+
return supertrend, direction
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
def swma(source: Series[float]) -> PyneFloat:
|
|
1894
|
+
"""
|
|
1895
|
+
Symmetrically weighted moving average with fixed length: 4. Weights: [1/6, 2/6, 2/6, 1/6].
|
|
1896
|
+
|
|
1897
|
+
:param source: The source series
|
|
1898
|
+
:return: The SWWMA of the source series
|
|
1899
|
+
"""
|
|
1900
|
+
if (isinstance(source, NA) or source != source):
|
|
1901
|
+
return na_float
|
|
1902
|
+
|
|
1903
|
+
return (source + 2 * source[1] + 2 * source[2] + source[3]) / 6
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
# noinspection PyUnusedLocal
|
|
1907
|
+
@module_function_property
|
|
1908
|
+
def tr(handle_na: bool = False) -> PyneFloat:
|
|
1909
|
+
"""
|
|
1910
|
+
Calculate True Range (TR)
|
|
1911
|
+
|
|
1912
|
+
:param handle_na: If true, and previous day's close is NaN then tr would be calculated as
|
|
1913
|
+
current day high-low. Otherwise (if false) tr would return NaN in such cases
|
|
1914
|
+
:return: True Range (TR)
|
|
1915
|
+
"""
|
|
1916
|
+
prev_close: Persistent[float] = na_float
|
|
1917
|
+
|
|
1918
|
+
if (isinstance(prev_close, NA) or prev_close != prev_close):
|
|
1919
|
+
val = (high - low) if handle_na else na_float
|
|
1920
|
+
else:
|
|
1921
|
+
val = builtins.max(high - low, abs(high - prev_close), abs(low - prev_close))
|
|
1922
|
+
|
|
1923
|
+
prev_close = close
|
|
1924
|
+
return val # type: ignore
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
def tsi(source: Series[float], short_length: int, long_length: int) -> PyneFloat:
|
|
1928
|
+
"""
|
|
1929
|
+
True strength index. It uses moving averages of the underlying momentum
|
|
1930
|
+
of a financial instrument.
|
|
1931
|
+
|
|
1932
|
+
:param source: Source series
|
|
1933
|
+
:param short_length: Short length
|
|
1934
|
+
:param long_length: Long length
|
|
1935
|
+
:return: True strength index between -1 and 1
|
|
1936
|
+
"""
|
|
1937
|
+
assert short_length > 0, "Invalid short length, must be greater than 0!"
|
|
1938
|
+
assert long_length > 0, "Invalid long length, must be greater than 0!"
|
|
1939
|
+
if (isinstance(source, NA) or source != source):
|
|
1940
|
+
return na_float
|
|
1941
|
+
|
|
1942
|
+
# Calculate momentum
|
|
1943
|
+
momentum = change(source)
|
|
1944
|
+
if (isinstance(momentum, NA) or momentum != momentum):
|
|
1945
|
+
return na_float
|
|
1946
|
+
|
|
1947
|
+
# First smooth both momentum and abs(momentum)
|
|
1948
|
+
momentum_ema = ema(momentum, long_length)
|
|
1949
|
+
abs_momentum_ema = ema(abs(momentum), long_length)
|
|
1950
|
+
|
|
1951
|
+
if (isinstance(momentum_ema, NA) or momentum_ema != momentum_ema) or (isinstance(abs_momentum_ema, NA) or abs_momentum_ema != abs_momentum_ema):
|
|
1952
|
+
return na_float
|
|
1953
|
+
|
|
1954
|
+
# Second smooth
|
|
1955
|
+
tsi_value = ema(momentum_ema, short_length)
|
|
1956
|
+
abs_value = ema(abs_momentum_ema, short_length)
|
|
1957
|
+
|
|
1958
|
+
if (isinstance(abs_value, NA) or abs_value != abs_value):
|
|
1959
|
+
return na_float
|
|
1960
|
+
|
|
1961
|
+
return tsi_value / abs_value
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def variance(source: Series[float],
|
|
1965
|
+
length: int,
|
|
1966
|
+
biased: bool = True) -> PyneFloat:
|
|
1967
|
+
"""
|
|
1968
|
+
Calculate the rolling variance of the source series.
|
|
1969
|
+
|
|
1970
|
+
:param source: The source series.
|
|
1971
|
+
:param length: The length of the rolling window.
|
|
1972
|
+
:param biased: If True, calculates biased variance; otherwise, calculates unbiased variance.
|
|
1973
|
+
:return: The variance of the source series.
|
|
1974
|
+
"""
|
|
1975
|
+
assert length > 0, "Invalid length, must be > 0!"
|
|
1976
|
+
length = int(length)
|
|
1977
|
+
if length == 1:
|
|
1978
|
+
return 0.0
|
|
1979
|
+
# Grow the buffer so the decremental read ``source[length]`` stays addressable for
|
|
1980
|
+
# lengths beyond the per-series default max_bars_back (500); otherwise it reads na and
|
|
1981
|
+
# poisons the Welford accumulators permanently (feeds stdev/bb/bbw/kc). Runs before the
|
|
1982
|
+
# na guard so the buffer keeps growing on na bars too, ahead of any wrap.
|
|
1983
|
+
max_bars_back(source, length)
|
|
1984
|
+
if (isinstance(source, NA) or source != source):
|
|
1985
|
+
return na_float
|
|
1986
|
+
|
|
1987
|
+
# Welford online recurrence with Pébay (2008) decremental step for sliding
|
|
1988
|
+
# window. Avoids the catastrophic cancellation of the textbook
|
|
1989
|
+
# `E[X²] - E[X]²` form by accumulating m2 (sum of squared deviations from
|
|
1990
|
+
# the running mean) directly. Kahan compensation on `mean` and `m2`
|
|
1991
|
+
# eliminates ULP-level drift across long add/remove sequences. The final
|
|
1992
|
+
# `max(0.0, ...)` is a safety clamp for the rare case where decremental
|
|
1993
|
+
# round-off lands one ULP below zero — `stdev()` calls `math.sqrt(var)`
|
|
1994
|
+
# and would crash on a negative argument.
|
|
1995
|
+
count: Persistent[int] = 0
|
|
1996
|
+
mu: Persistent[float] = 0.0
|
|
1997
|
+
mu_c: Persistent[float] = 0.0
|
|
1998
|
+
m2: Persistent[float] = 0.0
|
|
1999
|
+
m2_c: Persistent[float] = 0.0
|
|
2000
|
+
|
|
2001
|
+
# Add new sample (Welford incremental)
|
|
2002
|
+
if count < length:
|
|
2003
|
+
count += 1
|
|
2004
|
+
n_after = count
|
|
2005
|
+
else:
|
|
2006
|
+
count += 1
|
|
2007
|
+
n_after = length + 1 # transient: post-add, pre-remove
|
|
2008
|
+
|
|
2009
|
+
delta = source - mu
|
|
2010
|
+
inc = delta / n_after
|
|
2011
|
+
y = inc - mu_c
|
|
2012
|
+
t = mu + y
|
|
2013
|
+
mu_c = (t - mu) - y # noqa - it is persistent
|
|
2014
|
+
mu = t
|
|
2015
|
+
m2_inc = delta * (source - mu)
|
|
2016
|
+
y = m2_inc - m2_c
|
|
2017
|
+
t = m2 + y
|
|
2018
|
+
m2_c = (t - m2) - y # noqa - it is persistent
|
|
2019
|
+
m2 = t
|
|
2020
|
+
|
|
2021
|
+
if count < length:
|
|
2022
|
+
return na_float
|
|
2023
|
+
|
|
2024
|
+
if count > length:
|
|
2025
|
+
# Remove oldest sample (Welford decremental, Pébay 2008)
|
|
2026
|
+
old_value = source[length]
|
|
2027
|
+
delta = old_value - mu
|
|
2028
|
+
dec = -delta / length
|
|
2029
|
+
y = dec - mu_c
|
|
2030
|
+
t = mu + y
|
|
2031
|
+
mu_c = (t - mu) - y # noqa - it is persistent
|
|
2032
|
+
mu = t
|
|
2033
|
+
m2_dec = -delta * (old_value - mu)
|
|
2034
|
+
y = m2_dec - m2_c
|
|
2035
|
+
t = m2 + y
|
|
2036
|
+
m2_c = (t - m2) - y # noqa - it is persistent
|
|
2037
|
+
m2 = t
|
|
2038
|
+
|
|
2039
|
+
if biased:
|
|
2040
|
+
var = m2 / length
|
|
2041
|
+
else:
|
|
2042
|
+
var = m2 / (length - 1)
|
|
2043
|
+
# Safety clamp: decremental round-off can occasionally land 1 ULP below
|
|
2044
|
+
# zero on long constant runs; stdev() would crash inside math.sqrt().
|
|
2045
|
+
return builtins.max(0.0, var)
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def valuewhen(condition: bool, source: float, occurrence: int) -> PyneFloat:
|
|
2049
|
+
"""
|
|
2050
|
+
Returns the value of the source series when the condition is true for the given occurrence.
|
|
2051
|
+
|
|
2052
|
+
:param condition: The condition series
|
|
2053
|
+
:param source: The source series
|
|
2054
|
+
:param occurrence: The occurrence of the condition
|
|
2055
|
+
:return: The value of the source series when the condition is true for the given occurrence
|
|
2056
|
+
"""
|
|
2057
|
+
assert occurrence >= 0, "Invalid occurrence, must be >= 0!"
|
|
2058
|
+
if (isinstance(source, NA) or source != source):
|
|
2059
|
+
return na_float
|
|
2060
|
+
|
|
2061
|
+
values: Persistent[deque[float]] = deque(maxlen=occurrence + 1)
|
|
2062
|
+
|
|
2063
|
+
if condition:
|
|
2064
|
+
values.append(source)
|
|
2065
|
+
|
|
2066
|
+
if len(values) == occurrence + 1:
|
|
2067
|
+
return values[0]
|
|
2068
|
+
return na_float
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
# noinspection PyUnusedLocal
|
|
2072
|
+
@module_function_property
|
|
2073
|
+
def vwap(source: Series[float] | None = None, anchor: bool | None = None,
|
|
2074
|
+
stdev_mult: float | None = None) -> PyneFloat | tuple[PyneFloat, PyneFloat, PyneFloat]:
|
|
2075
|
+
"""
|
|
2076
|
+
Volume weighted average price.
|
|
2077
|
+
|
|
2078
|
+
Referenced bare (``ta.vwap``) this is the Pine built-in variable: the VWAP of
|
|
2079
|
+
``hlc3`` anchored to the session. Passing an explicit ``source`` selects the
|
|
2080
|
+
function form ``ta.vwap(source)``.
|
|
2081
|
+
|
|
2082
|
+
:param source: The source series; defaults to ``hlc3`` for the bare variable form
|
|
2083
|
+
:param anchor: The condition that triggers the reset of VWAP calculation
|
|
2084
|
+
:param stdev_mult: If specified, the function will calculate the standard deviation bands based on the main VWAP
|
|
2085
|
+
:return: The VWAP value or tuple of (vwap, upper_band, lower_band) if stdev_mult is specified
|
|
2086
|
+
"""
|
|
2087
|
+
if source is None:
|
|
2088
|
+
source = hlc3
|
|
2089
|
+
if (isinstance(source, NA) or source != source):
|
|
2090
|
+
return na_float if stdev_mult is None else (na_float, na_float, na_float)
|
|
2091
|
+
|
|
2092
|
+
# Persistent variables for calculation
|
|
2093
|
+
sum_vol: Persistent[float] = 0.0
|
|
2094
|
+
sum_pv: Persistent[float] = 0.0
|
|
2095
|
+
sum_ppv: Persistent[float] = 0.0
|
|
2096
|
+
had_anchor: Persistent[bool] = False
|
|
2097
|
+
|
|
2098
|
+
if anchor is None:
|
|
2099
|
+
anchor = session.isfirstbar
|
|
2100
|
+
|
|
2101
|
+
# Reset calculations if anchor condition is met
|
|
2102
|
+
if anchor is not None and anchor:
|
|
2103
|
+
sum_vol = volume
|
|
2104
|
+
sum_pv = source * volume
|
|
2105
|
+
sum_ppv = 0.0
|
|
2106
|
+
had_anchor = True
|
|
2107
|
+
# Only accumulate after first anchor
|
|
2108
|
+
elif had_anchor:
|
|
2109
|
+
sum_vol += volume
|
|
2110
|
+
sum_pv += source * volume
|
|
2111
|
+
else: # There was no anchor yet
|
|
2112
|
+
return na_float if stdev_mult is None else (na_float, na_float, na_float)
|
|
2113
|
+
|
|
2114
|
+
# Calculate VWAP
|
|
2115
|
+
vwap_value = sum_pv / sum_vol
|
|
2116
|
+
if (isinstance(vwap_value, NA) or vwap_value != vwap_value):
|
|
2117
|
+
return na_float if stdev_mult is None else (na_float, na_float, na_float)
|
|
2118
|
+
|
|
2119
|
+
# If stdev_mult is specified, calculate bands
|
|
2120
|
+
if had_anchor and stdev_mult is not None:
|
|
2121
|
+
sum_ppv += source * source * volume
|
|
2122
|
+
std = math.sqrt(builtins.max(0.0, sum_ppv / sum_vol - vwap_value * vwap_value))
|
|
2123
|
+
band_width = std * stdev_mult
|
|
2124
|
+
# Return tuple of (vwap, upper_band, lower_band)
|
|
2125
|
+
return vwap_value, vwap_value + band_width, vwap_value - band_width
|
|
2126
|
+
|
|
2127
|
+
return vwap_value
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
def vwma(source: float, length: int) -> PyneFloat:
|
|
2131
|
+
return sma(source * volume, length) / sma(volume, length)
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
# noinspection PyUnusedLocal
|
|
2135
|
+
@module_property
|
|
2136
|
+
def wad() -> PyneFloat:
|
|
2137
|
+
"""
|
|
2138
|
+
Williams Accumulation/Distribution.
|
|
2139
|
+
|
|
2140
|
+
:return: Williams Accumulation/Distribution
|
|
2141
|
+
"""
|
|
2142
|
+
prev_close: Persistent[float] = na_float
|
|
2143
|
+
true_high = builtins.max(high, prev_close)
|
|
2144
|
+
true_low = builtins.min(low, prev_close)
|
|
2145
|
+
momentum = close - prev_close
|
|
2146
|
+
gain = (close - true_low) if momentum > 0.0 else ((close - true_high) if momentum < 0.0 else 0.0)
|
|
2147
|
+
prev_close = close
|
|
2148
|
+
return cum(gain)
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
def wma(source: Series[float], length: int) -> PyneFloat:
|
|
2152
|
+
"""
|
|
2153
|
+
Calculate the Weighted Moving Average (WMA) of the source series with the given length.
|
|
2154
|
+
|
|
2155
|
+
:param source: The source series
|
|
2156
|
+
:param length: The length of the WMA
|
|
2157
|
+
:return: The WMA of the source series
|
|
2158
|
+
"""
|
|
2159
|
+
assert length > 0, "Invalid length, length must be greater than 0!"
|
|
2160
|
+
length = int(length)
|
|
2161
|
+
|
|
2162
|
+
# Calculate denominator only once
|
|
2163
|
+
denom: Persistent[float] = length * (length + 1) / 2
|
|
2164
|
+
|
|
2165
|
+
count: Persistent[int] = 0
|
|
2166
|
+
summ: Persistent[float] = 0.0
|
|
2167
|
+
weighted_summ: Persistent[float] = 0.0
|
|
2168
|
+
|
|
2169
|
+
if (isinstance(source, NA) or source != source):
|
|
2170
|
+
# An NA bar leaves the window unchanged; hold the last full value
|
|
2171
|
+
# (still NA while warming up)
|
|
2172
|
+
return na_float if count < length else weighted_summ / denom
|
|
2173
|
+
|
|
2174
|
+
# NA values are NOT stored in the buffer, only skipped, so ``src[length]``
|
|
2175
|
+
# indexes past NA gaps to the true oldest value still inside the window.
|
|
2176
|
+
# Reading the parameter directly would step back ``length`` *bars* and land
|
|
2177
|
+
# inside an NA gap, subtracting an NA that poisons ``summ`` forever.
|
|
2178
|
+
src: Series[float] = source
|
|
2179
|
+
# Grow the na-compacted buffer so ``src[length]`` stays addressable for lengths
|
|
2180
|
+
# beyond the per-series default max_bars_back (500); otherwise the window-drop
|
|
2181
|
+
# read returns na and poisons ``summ`` permanently.
|
|
2182
|
+
max_bars_back(src, int(length))
|
|
2183
|
+
|
|
2184
|
+
# Warming up phase — only non-NA samples advance the window
|
|
2185
|
+
if count < length:
|
|
2186
|
+
count += 1
|
|
2187
|
+
summ += source
|
|
2188
|
+
weighted_summ += source * count
|
|
2189
|
+
if count < length:
|
|
2190
|
+
return na_float
|
|
2191
|
+
|
|
2192
|
+
# Normal calculation phase
|
|
2193
|
+
else:
|
|
2194
|
+
old_summ = summ
|
|
2195
|
+
# Substract the oldest value and add the newest value
|
|
2196
|
+
summ -= src[length] - source
|
|
2197
|
+
# Substract the oldest weighted value and add the newest weighted value
|
|
2198
|
+
weighted_summ -= old_summ - length * source
|
|
2199
|
+
|
|
2200
|
+
val = weighted_summ / denom
|
|
2201
|
+
return val # type: ignore
|
|
2202
|
+
|
|
2203
|
+
|
|
2204
|
+
def wpr(length: int) -> PyneFloat:
|
|
2205
|
+
"""
|
|
2206
|
+
Williams %R indicator.
|
|
2207
|
+
|
|
2208
|
+
:param length: Length of the indicator
|
|
2209
|
+
:return: Williams %R value
|
|
2210
|
+
"""
|
|
2211
|
+
assert length > 0, "Invalid length, must be greater than 0!"
|
|
2212
|
+
length = int(length)
|
|
2213
|
+
|
|
2214
|
+
if length == 1:
|
|
2215
|
+
return close
|
|
2216
|
+
|
|
2217
|
+
hmax = highest(high, length)
|
|
2218
|
+
lmin = lowest(low, length)
|
|
2219
|
+
|
|
2220
|
+
return 100 * (close - hmax) / (hmax - lmin)
|
|
2221
|
+
|
|
2222
|
+
|
|
2223
|
+
@module_property
|
|
2224
|
+
def wvad() -> PyneFloat:
|
|
2225
|
+
"""
|
|
2226
|
+
Weighted Volume Accumulation/Distribution.
|
|
2227
|
+
|
|
2228
|
+
:return: Weighted Volume Accumulation/Distribution
|
|
2229
|
+
"""
|
|
2230
|
+
return (close - open) / (high - low) * volume
|