oakscriptpy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oakscriptpy/__init__.py +93 -0
- oakscriptpy/_metadata.py +118 -0
- oakscriptpy/_types.py +185 -0
- oakscriptpy/_utils.py +145 -0
- oakscriptpy/adapters/__init__.py +5 -0
- oakscriptpy/adapters/simple_input.py +63 -0
- oakscriptpy/array.py +342 -0
- oakscriptpy/box.py +151 -0
- oakscriptpy/chartpoint.py +21 -0
- oakscriptpy/color.py +134 -0
- oakscriptpy/indicator.py +82 -0
- oakscriptpy/input_.py +38 -0
- oakscriptpy/inputs.py +170 -0
- oakscriptpy/label.py +110 -0
- oakscriptpy/lib/__init__.py +5 -0
- oakscriptpy/lib/zigzag.py +158 -0
- oakscriptpy/line.py +120 -0
- oakscriptpy/linefill.py +26 -0
- oakscriptpy/math_.py +184 -0
- oakscriptpy/matrix.py +1136 -0
- oakscriptpy/plot_.py +49 -0
- oakscriptpy/polyline.py +60 -0
- oakscriptpy/runtime.py +150 -0
- oakscriptpy/runtime_types.py +52 -0
- oakscriptpy/series.py +292 -0
- oakscriptpy/str_.py +166 -0
- oakscriptpy/ta.py +1795 -0
- oakscriptpy/ta_series.py +353 -0
- oakscriptpy/time_.py +22 -0
- oakscriptpy-0.1.0.dist-info/METADATA +120 -0
- oakscriptpy-0.1.0.dist-info/RECORD +32 -0
- oakscriptpy-0.1.0.dist-info/WHEEL +4 -0
oakscriptpy/indicator.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Indicator factory — creates reusable indicator instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from ._types import Bar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class IndicatorMetadataConfig:
|
|
13
|
+
title: str
|
|
14
|
+
short_title: str | None = None
|
|
15
|
+
overlay: bool = False
|
|
16
|
+
format: str = "price"
|
|
17
|
+
precision: int = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class IndicatorContext:
|
|
22
|
+
data: list[Bar]
|
|
23
|
+
open: list[float]
|
|
24
|
+
high: list[float]
|
|
25
|
+
low: list[float]
|
|
26
|
+
close: list[float]
|
|
27
|
+
volume: list[float]
|
|
28
|
+
time: list[int]
|
|
29
|
+
pane_index: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IndicatorInstance:
|
|
33
|
+
def __init__(self, metadata: IndicatorMetadataConfig, setup: Callable[[IndicatorContext], None]) -> None:
|
|
34
|
+
self.metadata = metadata
|
|
35
|
+
self._setup = setup
|
|
36
|
+
self._pane_index = 0 if metadata.overlay else 1
|
|
37
|
+
self._input_values: dict[str, Any] = {}
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def pane_index(self) -> int:
|
|
41
|
+
return self._pane_index
|
|
42
|
+
|
|
43
|
+
def is_overlay(self) -> bool:
|
|
44
|
+
return self.metadata.overlay
|
|
45
|
+
|
|
46
|
+
def update_inputs(self, inputs: dict[str, Any]) -> None:
|
|
47
|
+
self._input_values.update(inputs)
|
|
48
|
+
|
|
49
|
+
def get_input_values(self) -> dict[str, Any]:
|
|
50
|
+
return dict(self._input_values)
|
|
51
|
+
|
|
52
|
+
def calculate(self, data: list[Bar]) -> None:
|
|
53
|
+
ctx = IndicatorContext(
|
|
54
|
+
data=data,
|
|
55
|
+
open=[b.open for b in data],
|
|
56
|
+
high=[b.high for b in data],
|
|
57
|
+
low=[b.low for b in data],
|
|
58
|
+
close=[b.close for b in data],
|
|
59
|
+
volume=[b.volume for b in data],
|
|
60
|
+
time=[b.time for b in data],
|
|
61
|
+
pane_index=self._pane_index,
|
|
62
|
+
)
|
|
63
|
+
self._setup(ctx)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def indicator(
|
|
67
|
+
metadata: IndicatorMetadataConfig,
|
|
68
|
+
setup: Callable[[IndicatorContext], None],
|
|
69
|
+
) -> Callable[[], IndicatorInstance]:
|
|
70
|
+
"""Create an indicator factory function."""
|
|
71
|
+
normalized = IndicatorMetadataConfig(
|
|
72
|
+
title=metadata.title,
|
|
73
|
+
short_title=metadata.short_title or metadata.title,
|
|
74
|
+
overlay=metadata.overlay,
|
|
75
|
+
format=metadata.format,
|
|
76
|
+
precision=metadata.precision,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def factory() -> IndicatorInstance:
|
|
80
|
+
return IndicatorInstance(normalized, setup)
|
|
81
|
+
|
|
82
|
+
return factory
|
oakscriptpy/input_.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Input helper — creates InputValue dataclass instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class InputValue:
|
|
11
|
+
type: str
|
|
12
|
+
default_value: Any
|
|
13
|
+
value: Any
|
|
14
|
+
title: str | None = None
|
|
15
|
+
min: float | None = None
|
|
16
|
+
max: float | None = None
|
|
17
|
+
step: float | None = None
|
|
18
|
+
options: list[str] | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def int_(default_value: int, title: str | None = None, min: int | None = None, max: int | None = None, step: int = 1) -> InputValue:
|
|
22
|
+
return InputValue(type="int", default_value=default_value, value=default_value, title=title, min=float(min) if min is not None else None, max=float(max) if max is not None else None, step=float(step))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def float_(default_value: float, title: str | None = None, min: float | None = None, max: float | None = None, step: float = 0.1) -> InputValue:
|
|
26
|
+
return InputValue(type="float", default_value=default_value, value=default_value, title=title, min=min, max=max, step=step)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def source(default_value: str = "close", title: str | None = None) -> InputValue:
|
|
30
|
+
return InputValue(type="source", default_value=default_value, value=default_value, title=title, options=["open", "high", "low", "close", "hl2", "hlc3", "ohlc4", "hlcc4"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def bool_(default_value: bool, title: str | None = None) -> InputValue:
|
|
34
|
+
return InputValue(type="bool", default_value=default_value, value=default_value, title=title)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def string(default_value: str, title: str | None = None, options: list[str] | None = None) -> InputValue:
|
|
38
|
+
return InputValue(type="string", default_value=default_value, value=default_value, title=title, options=options)
|
oakscriptpy/inputs.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Input functions — mirrors PineScript input.* with idempotent semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .runtime import get_context, recalculate
|
|
9
|
+
from .runtime_types import InputConfig
|
|
10
|
+
|
|
11
|
+
_registered_inputs: dict[str, bool] = {}
|
|
12
|
+
_auto_recalculate_enabled: bool = False
|
|
13
|
+
_input_counter: int = 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def enable_auto_recalculate() -> None:
|
|
17
|
+
global _auto_recalculate_enabled
|
|
18
|
+
_auto_recalculate_enabled = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def disable_auto_recalculate() -> None:
|
|
22
|
+
global _auto_recalculate_enabled
|
|
23
|
+
_auto_recalculate_enabled = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def reset_inputs() -> None:
|
|
27
|
+
global _input_counter
|
|
28
|
+
_registered_inputs.clear()
|
|
29
|
+
_input_counter = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _generate_input_id(title: str | None, defval: Any, type_: str) -> str:
|
|
33
|
+
global _input_counter
|
|
34
|
+
if title:
|
|
35
|
+
return title.replace(" ", "_")
|
|
36
|
+
id_ = f"{type_}_{_input_counter}_{defval}"
|
|
37
|
+
_input_counter += 1
|
|
38
|
+
return id_
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def input_int(
|
|
42
|
+
defval: int, title: str | None = None,
|
|
43
|
+
min: int | None = None, max: int | None = None, step: int = 1,
|
|
44
|
+
) -> int:
|
|
45
|
+
ctx = get_context()
|
|
46
|
+
if ctx is None:
|
|
47
|
+
return int(defval)
|
|
48
|
+
|
|
49
|
+
id_ = _generate_input_id(title, defval, "int")
|
|
50
|
+
config = InputConfig(
|
|
51
|
+
id=id_, type="int", defval=int(defval), title=title,
|
|
52
|
+
min=float(min) if min is not None else None,
|
|
53
|
+
max=float(max) if max is not None else None,
|
|
54
|
+
step=float(step),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if id_ not in _registered_inputs:
|
|
58
|
+
ctx.inputs.register_input(config)
|
|
59
|
+
_registered_inputs[id_] = True
|
|
60
|
+
if _auto_recalculate_enabled:
|
|
61
|
+
ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
|
|
62
|
+
|
|
63
|
+
value = ctx.inputs.get_value(id_)
|
|
64
|
+
return int(value) if isinstance(value, (int, float)) else int(defval)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def input_float(
|
|
68
|
+
defval: float, title: str | None = None,
|
|
69
|
+
min: float | None = None, max: float | None = None, step: float = 0.1,
|
|
70
|
+
) -> float:
|
|
71
|
+
ctx = get_context()
|
|
72
|
+
if ctx is None:
|
|
73
|
+
return defval
|
|
74
|
+
|
|
75
|
+
id_ = _generate_input_id(title, defval, "float")
|
|
76
|
+
config = InputConfig(
|
|
77
|
+
id=id_, type="float", defval=defval, title=title,
|
|
78
|
+
min=min, max=max, step=step,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if id_ not in _registered_inputs:
|
|
82
|
+
ctx.inputs.register_input(config)
|
|
83
|
+
_registered_inputs[id_] = True
|
|
84
|
+
if _auto_recalculate_enabled:
|
|
85
|
+
ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
|
|
86
|
+
|
|
87
|
+
value = ctx.inputs.get_value(id_)
|
|
88
|
+
return float(value) if isinstance(value, (int, float)) else defval
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def input_bool(defval: bool, title: str | None = None) -> bool:
|
|
92
|
+
ctx = get_context()
|
|
93
|
+
if ctx is None:
|
|
94
|
+
return defval
|
|
95
|
+
|
|
96
|
+
id_ = _generate_input_id(title, defval, "bool")
|
|
97
|
+
config = InputConfig(id=id_, type="bool", defval=defval, title=title)
|
|
98
|
+
|
|
99
|
+
if id_ not in _registered_inputs:
|
|
100
|
+
ctx.inputs.register_input(config)
|
|
101
|
+
_registered_inputs[id_] = True
|
|
102
|
+
if _auto_recalculate_enabled:
|
|
103
|
+
ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
|
|
104
|
+
|
|
105
|
+
value = ctx.inputs.get_value(id_)
|
|
106
|
+
return bool(value) if isinstance(value, bool) else defval
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def input_string(
|
|
110
|
+
defval: str, title: str | None = None, options: list[str] | None = None,
|
|
111
|
+
) -> str:
|
|
112
|
+
ctx = get_context()
|
|
113
|
+
if ctx is None:
|
|
114
|
+
return defval
|
|
115
|
+
|
|
116
|
+
id_ = _generate_input_id(title, defval, "string")
|
|
117
|
+
config = InputConfig(id=id_, type="string", defval=defval, title=title, options=options)
|
|
118
|
+
|
|
119
|
+
if id_ not in _registered_inputs:
|
|
120
|
+
ctx.inputs.register_input(config)
|
|
121
|
+
_registered_inputs[id_] = True
|
|
122
|
+
if _auto_recalculate_enabled:
|
|
123
|
+
ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
|
|
124
|
+
|
|
125
|
+
value = ctx.inputs.get_value(id_)
|
|
126
|
+
return str(value) if isinstance(value, str) else defval
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def input_source(defval: str = "close", title: str | None = None) -> list[float]:
|
|
130
|
+
ctx = get_context()
|
|
131
|
+
if ctx is None:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
id_ = _generate_input_id(title, defval, "source")
|
|
135
|
+
config = InputConfig(
|
|
136
|
+
id=id_, type="source", defval=defval, title=title,
|
|
137
|
+
options=["open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4"],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if id_ not in _registered_inputs:
|
|
141
|
+
ctx.inputs.register_input(config)
|
|
142
|
+
_registered_inputs[id_] = True
|
|
143
|
+
if _auto_recalculate_enabled:
|
|
144
|
+
ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
|
|
145
|
+
|
|
146
|
+
source_name = ctx.inputs.get_value(id_)
|
|
147
|
+
if not isinstance(source_name, str):
|
|
148
|
+
source_name = defval
|
|
149
|
+
|
|
150
|
+
return _get_source_data(ctx.ohlcv, source_name)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _get_source_data(ohlcv: Any, source_name: str) -> list[float]:
|
|
154
|
+
if source_name == "open":
|
|
155
|
+
return ohlcv.open
|
|
156
|
+
if source_name == "high":
|
|
157
|
+
return ohlcv.high
|
|
158
|
+
if source_name == "low":
|
|
159
|
+
return ohlcv.low
|
|
160
|
+
if source_name == "close":
|
|
161
|
+
return ohlcv.close
|
|
162
|
+
if source_name == "volume":
|
|
163
|
+
return ohlcv.volume
|
|
164
|
+
if source_name == "hl2":
|
|
165
|
+
return [(h + l) / 2 for h, l in zip(ohlcv.high, ohlcv.low)]
|
|
166
|
+
if source_name == "hlc3":
|
|
167
|
+
return [(h + l + c) / 3 for h, l, c in zip(ohlcv.high, ohlcv.low, ohlcv.close)]
|
|
168
|
+
if source_name == "ohlc4":
|
|
169
|
+
return [(o + h + l + c) / 4 for o, h, l, c in zip(ohlcv.open, ohlcv.high, ohlcv.low, ohlcv.close)]
|
|
170
|
+
return ohlcv.close
|
oakscriptpy/label.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Label namespace — mirrors PineScript label.* functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy as _copy
|
|
6
|
+
|
|
7
|
+
from ._types import Label
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def new(
|
|
11
|
+
x: float, y: float, text: str | None = None,
|
|
12
|
+
xloc: str = "bar_index", yloc: str = "price",
|
|
13
|
+
color: str | int | None = None, style: str | None = None,
|
|
14
|
+
textcolor: str | int | None = None, size: str | int | None = None,
|
|
15
|
+
textalign: str | None = None, tooltip: str | None = None,
|
|
16
|
+
text_font_family: str | None = None,
|
|
17
|
+
) -> Label:
|
|
18
|
+
return Label(
|
|
19
|
+
x=x, y=y, xloc=xloc, yloc=yloc, text=text, tooltip=tooltip,
|
|
20
|
+
color=color, style=style, textcolor=textcolor, size=size,
|
|
21
|
+
textalign=textalign, text_font_family=text_font_family,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_x(id: Label) -> float:
|
|
26
|
+
return id.x
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_y(id: Label) -> float:
|
|
30
|
+
return id.y
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_text(id: Label) -> str | None:
|
|
34
|
+
return id.text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def copy(id: Label) -> Label:
|
|
38
|
+
return _copy.copy(id)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def set_x(id: Label, x: float) -> Label:
|
|
42
|
+
id.x = x
|
|
43
|
+
return id
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def set_y(id: Label, y: float) -> Label:
|
|
47
|
+
id.y = y
|
|
48
|
+
return id
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_xy(id: Label, x: float, y: float) -> Label:
|
|
52
|
+
id.x = x
|
|
53
|
+
id.y = y
|
|
54
|
+
return id
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def set_xloc(id: Label, x: float, xloc: str) -> Label:
|
|
58
|
+
id.x = x
|
|
59
|
+
id.xloc = xloc
|
|
60
|
+
return id
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def set_yloc(id: Label, y: float, yloc: str) -> Label:
|
|
64
|
+
id.y = y
|
|
65
|
+
id.yloc = yloc
|
|
66
|
+
return id
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def set_text(id: Label, text: str) -> Label:
|
|
70
|
+
id.text = text
|
|
71
|
+
return id
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def set_tooltip(id: Label, tooltip: str) -> Label:
|
|
75
|
+
id.tooltip = tooltip
|
|
76
|
+
return id
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_color(id: Label, color: str | int) -> Label:
|
|
80
|
+
id.color = color
|
|
81
|
+
return id
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_textcolor(id: Label, color: str | int) -> Label:
|
|
85
|
+
id.textcolor = color
|
|
86
|
+
return id
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def set_style(id: Label, style: str) -> Label:
|
|
90
|
+
id.style = style
|
|
91
|
+
return id
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_size(id: Label, size: str | int) -> Label:
|
|
95
|
+
id.size = size
|
|
96
|
+
return id
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def set_textalign(id: Label, align: str) -> Label:
|
|
100
|
+
id.textalign = align
|
|
101
|
+
return id
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def set_text_font_family(id: Label, font: str) -> Label:
|
|
105
|
+
id.text_font_family = font
|
|
106
|
+
return id
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def delete(id: Label) -> None:
|
|
110
|
+
pass
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""ZigZag library — identifies trend reversals via pivot highs and lows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._types import Bar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ZigZagSettings:
|
|
13
|
+
dev_threshold: float = 5.0
|
|
14
|
+
depth: int = 10
|
|
15
|
+
line_color: str = "#2962FF"
|
|
16
|
+
extend_last: bool = True
|
|
17
|
+
display_reversal_price: bool = True
|
|
18
|
+
display_cumulative_volume: bool = True
|
|
19
|
+
display_reversal_price_change: bool = True
|
|
20
|
+
difference_price_mode: str = "Absolute"
|
|
21
|
+
allow_zigzag_on_one_bar: bool = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ZigZagPoint:
|
|
26
|
+
time: int
|
|
27
|
+
bar_index: int
|
|
28
|
+
price: float
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ZigZagPivot:
|
|
33
|
+
is_high: bool
|
|
34
|
+
volume: float
|
|
35
|
+
start: ZigZagPoint
|
|
36
|
+
end: ZigZagPoint
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ZigZagResult:
|
|
41
|
+
pivots: list[ZigZagPivot]
|
|
42
|
+
extension: ZigZagPivot | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ZigZag:
|
|
46
|
+
def __init__(self, settings: ZigZagSettings | None = None) -> None:
|
|
47
|
+
self.settings = settings or ZigZagSettings()
|
|
48
|
+
self.pivots: list[ZigZagPivot] = []
|
|
49
|
+
self.sum_vol: float = 0.0
|
|
50
|
+
self._high_buffer: list[float] = []
|
|
51
|
+
self._low_buffer: list[float] = []
|
|
52
|
+
self._time_buffer: list[int] = []
|
|
53
|
+
self._bar_count: int = 0
|
|
54
|
+
|
|
55
|
+
def update(self, bar: Bar, bar_index: int) -> bool:
|
|
56
|
+
self._high_buffer.append(bar.high)
|
|
57
|
+
self._low_buffer.append(bar.low)
|
|
58
|
+
self._time_buffer.append(bar.time)
|
|
59
|
+
self._bar_count += 1
|
|
60
|
+
|
|
61
|
+
depth = max(2, self.settings.depth // 2)
|
|
62
|
+
self.sum_vol += bar.volume
|
|
63
|
+
|
|
64
|
+
if self._bar_count < depth * 2 + 1:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
changed = self._try_find_pivot(True, depth, bar_index)
|
|
68
|
+
changed = self._try_find_pivot(
|
|
69
|
+
False, depth, bar_index,
|
|
70
|
+
self.settings.allow_zigzag_on_one_bar or not changed,
|
|
71
|
+
) or changed
|
|
72
|
+
return changed
|
|
73
|
+
|
|
74
|
+
def last_pivot(self) -> ZigZagPivot | None:
|
|
75
|
+
return self.pivots[-1] if self.pivots else None
|
|
76
|
+
|
|
77
|
+
def get_extension(self, current_bar: Bar, bar_index: int) -> ZigZagPivot | None:
|
|
78
|
+
if not self.settings.extend_last:
|
|
79
|
+
return None
|
|
80
|
+
last = self.last_pivot()
|
|
81
|
+
if last is None:
|
|
82
|
+
return None
|
|
83
|
+
is_high = not last.is_high
|
|
84
|
+
price = current_bar.high if is_high else current_bar.low
|
|
85
|
+
return ZigZagPivot(
|
|
86
|
+
is_high=is_high, volume=self.sum_vol,
|
|
87
|
+
start=last.end, end=ZigZagPoint(time=current_bar.time, bar_index=bar_index, price=price),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _try_find_pivot(self, is_high: bool, depth: int, current_bar_index: int, register: bool = True) -> bool:
|
|
91
|
+
point = self._find_pivot_point(is_high, depth, current_bar_index)
|
|
92
|
+
if point is None or not register:
|
|
93
|
+
return False
|
|
94
|
+
return self._new_pivot_found(is_high, point)
|
|
95
|
+
|
|
96
|
+
def _find_pivot_point(self, is_high: bool, depth: int, current_bar_index: int) -> ZigZagPoint | None:
|
|
97
|
+
buffer = self._high_buffer if is_high else self._low_buffer
|
|
98
|
+
pivot_idx = len(buffer) - 1 - depth
|
|
99
|
+
if pivot_idx < depth:
|
|
100
|
+
return None
|
|
101
|
+
pivot_price = buffer[pivot_idx]
|
|
102
|
+
|
|
103
|
+
for i in range(pivot_idx + 1, len(buffer)):
|
|
104
|
+
p = buffer[i]
|
|
105
|
+
if is_high and p > pivot_price:
|
|
106
|
+
return None
|
|
107
|
+
if not is_high and p < pivot_price:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
for i in range(pivot_idx - depth, pivot_idx):
|
|
111
|
+
p = buffer[i]
|
|
112
|
+
if is_high and p >= pivot_price:
|
|
113
|
+
return None
|
|
114
|
+
if not is_high and p <= pivot_price:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
return ZigZagPoint(
|
|
118
|
+
time=self._time_buffer[pivot_idx],
|
|
119
|
+
bar_index=current_bar_index - depth,
|
|
120
|
+
price=pivot_price,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _calc_dev(self, base_price: float, price: float) -> float:
|
|
124
|
+
return 100 * (price - base_price) / abs(base_price) if base_price != 0 else 0.0
|
|
125
|
+
|
|
126
|
+
def _new_pivot_found(self, is_high: bool, point: ZigZagPoint) -> bool:
|
|
127
|
+
last = self.last_pivot()
|
|
128
|
+
if last is None:
|
|
129
|
+
self.pivots.append(ZigZagPivot(is_high=is_high, volume=self.sum_vol, start=point, end=point))
|
|
130
|
+
self.sum_vol = 0.0
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
if last.is_high == is_high:
|
|
134
|
+
more_extreme = (point.price > last.end.price) if is_high else (point.price < last.end.price)
|
|
135
|
+
if more_extreme:
|
|
136
|
+
last.end = point
|
|
137
|
+
last.volume += self.sum_vol
|
|
138
|
+
self.sum_vol = 0.0
|
|
139
|
+
return True
|
|
140
|
+
else:
|
|
141
|
+
dev = self._calc_dev(last.end.price, point.price)
|
|
142
|
+
threshold = self.settings.dev_threshold
|
|
143
|
+
meets = (dev <= -threshold) if last.is_high else (dev >= threshold)
|
|
144
|
+
if meets:
|
|
145
|
+
self.pivots.append(ZigZagPivot(is_high=is_high, volume=self.sum_vol, start=last.end, end=point))
|
|
146
|
+
self.sum_vol = 0.0
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def calculate_zigzag(bars: list[Bar], settings: ZigZagSettings | None = None) -> ZigZagResult:
|
|
152
|
+
if not bars:
|
|
153
|
+
return ZigZagResult(pivots=[], extension=None)
|
|
154
|
+
zz = ZigZag(settings)
|
|
155
|
+
for i, bar in enumerate(bars):
|
|
156
|
+
zz.update(bar, i)
|
|
157
|
+
extension = zz.get_extension(bars[-1], len(bars) - 1)
|
|
158
|
+
return ZigZagResult(pivots=zz.pivots, extension=extension)
|
oakscriptpy/line.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Line namespace — mirrors PineScript line.* functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy as _copy
|
|
6
|
+
|
|
7
|
+
from ._types import Line
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def new(
|
|
11
|
+
x1: float, y1: float, x2: float, y2: float,
|
|
12
|
+
xloc: str = "bar_index", extend: str = "none",
|
|
13
|
+
color: str | int | None = None, style: str | None = None, width: int | None = None,
|
|
14
|
+
) -> Line:
|
|
15
|
+
return Line(x1=x1, y1=y1, x2=x2, y2=y2, xloc=xloc, extend=extend,
|
|
16
|
+
color=color, style=style or "solid", width=width or 1)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_price(id: Line, x: float) -> float:
|
|
20
|
+
if id.xloc == "bar_time":
|
|
21
|
+
raise ValueError("line.get_price() only works with xloc.bar_index lines")
|
|
22
|
+
if id.x2 == id.x1:
|
|
23
|
+
return float("nan")
|
|
24
|
+
slope = (id.y2 - id.y1) / (id.x2 - id.x1)
|
|
25
|
+
price = id.y1 + slope * (x - id.x1)
|
|
26
|
+
lo = min(id.x1, id.x2)
|
|
27
|
+
hi = max(id.x1, id.x2)
|
|
28
|
+
if id.extend == "none":
|
|
29
|
+
if x < lo or x > hi:
|
|
30
|
+
return float("nan")
|
|
31
|
+
elif id.extend == "left":
|
|
32
|
+
if x > hi:
|
|
33
|
+
return float("nan")
|
|
34
|
+
elif id.extend == "right":
|
|
35
|
+
if x < lo:
|
|
36
|
+
return float("nan")
|
|
37
|
+
return price
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_x1(id: Line) -> float:
|
|
41
|
+
return id.x1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_x2(id: Line) -> float:
|
|
45
|
+
return id.x2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_y1(id: Line) -> float:
|
|
49
|
+
return id.y1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_y2(id: Line) -> float:
|
|
53
|
+
return id.y2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def copy(id: Line) -> Line:
|
|
57
|
+
return _copy.copy(id)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def set_x1(id: Line, x: float) -> Line:
|
|
61
|
+
id.x1 = x
|
|
62
|
+
return id
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def set_x2(id: Line, x: float) -> Line:
|
|
66
|
+
id.x2 = x
|
|
67
|
+
return id
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def set_y1(id: Line, y: float) -> Line:
|
|
71
|
+
id.y1 = y
|
|
72
|
+
return id
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_y2(id: Line, y: float) -> Line:
|
|
76
|
+
id.y2 = y
|
|
77
|
+
return id
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def set_xy1(id: Line, x: float, y: float) -> Line:
|
|
81
|
+
id.x1 = x
|
|
82
|
+
id.y1 = y
|
|
83
|
+
return id
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def set_xy2(id: Line, x: float, y: float) -> Line:
|
|
87
|
+
id.x2 = x
|
|
88
|
+
id.y2 = y
|
|
89
|
+
return id
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def set_xloc(id: Line, x1: float, x2: float, xloc: str) -> Line:
|
|
93
|
+
id.x1 = x1
|
|
94
|
+
id.x2 = x2
|
|
95
|
+
id.xloc = xloc
|
|
96
|
+
return id
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def set_extend(id: Line, extend: str) -> Line:
|
|
100
|
+
id.extend = extend
|
|
101
|
+
return id
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def set_color(id: Line, color: str | int) -> Line:
|
|
105
|
+
id.color = color
|
|
106
|
+
return id
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def set_style(id: Line, style: str) -> Line:
|
|
110
|
+
id.style = style
|
|
111
|
+
return id
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def set_width(id: Line, width: int) -> Line:
|
|
115
|
+
id.width = width
|
|
116
|
+
return id
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def delete(id: Line) -> None:
|
|
120
|
+
pass
|
oakscriptpy/linefill.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Linefill namespace — mirrors PineScript linefill.* functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._types import Linefill, Line
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def new(line1: Line, line2: Line, color: str | int | None = None) -> Linefill:
|
|
9
|
+
return Linefill(line1=line1, line2=line2, color=color)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_line1(id: Linefill) -> Line:
|
|
13
|
+
return id.line1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_line2(id: Linefill) -> Line:
|
|
17
|
+
return id.line2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_color(id: Linefill, color: str | int) -> Linefill:
|
|
21
|
+
id.color = color
|
|
22
|
+
return id
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def delete(id: Linefill) -> None:
|
|
26
|
+
pass
|