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/plot_.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Plot helper — prepares data for charting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TimeValuePair:
|
|
12
|
+
time: int | str
|
|
13
|
+
value: float
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PlotResult:
|
|
18
|
+
id: str
|
|
19
|
+
data: list[TimeValuePair]
|
|
20
|
+
options: dict[str, Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def plot(
|
|
24
|
+
values: list[float | None],
|
|
25
|
+
times: list[int | str],
|
|
26
|
+
color: str | None = None,
|
|
27
|
+
title: str | None = None,
|
|
28
|
+
line_width: int | None = None,
|
|
29
|
+
line_style: str | None = None,
|
|
30
|
+
offset: int = 0,
|
|
31
|
+
) -> list[TimeValuePair]:
|
|
32
|
+
result: list[TimeValuePair] = []
|
|
33
|
+
for i, v in enumerate(values):
|
|
34
|
+
if v is None or (isinstance(v, float) and math.isnan(v)):
|
|
35
|
+
continue
|
|
36
|
+
time_index = i + offset
|
|
37
|
+
if time_index < 0 or time_index >= len(times):
|
|
38
|
+
continue
|
|
39
|
+
result.append(TimeValuePair(time=times[time_index], value=v))
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create_plot(
|
|
44
|
+
id: str,
|
|
45
|
+
values: list[float | None],
|
|
46
|
+
times: list[int | str],
|
|
47
|
+
**options: Any,
|
|
48
|
+
) -> PlotResult:
|
|
49
|
+
return PlotResult(id=id, data=plot(values, times, **options), options=options)
|
oakscriptpy/polyline.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Polyline namespace — mirrors PineScript polyline.* functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
from ._types import ChartPoint, Polyline
|
|
8
|
+
|
|
9
|
+
_polyline_id_counter = 0
|
|
10
|
+
_all_polylines: list[Polyline] = []
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _generate_id() -> str:
|
|
14
|
+
global _polyline_id_counter
|
|
15
|
+
_polyline_id_counter += 1
|
|
16
|
+
return f"polyline_{_polyline_id_counter}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def new(
|
|
20
|
+
points: list[ChartPoint],
|
|
21
|
+
curved: bool = False,
|
|
22
|
+
closed: bool = False,
|
|
23
|
+
xloc: str = "bar_index",
|
|
24
|
+
line_color: str | int = "#2196F3",
|
|
25
|
+
fill_color: str | int | None = None,
|
|
26
|
+
line_style: str = "solid",
|
|
27
|
+
line_width: int = 1,
|
|
28
|
+
force_overlay: bool = False,
|
|
29
|
+
) -> Polyline:
|
|
30
|
+
if curved:
|
|
31
|
+
warnings.warn("polyline.new(): curved polylines not yet supported. Using straight lines.")
|
|
32
|
+
if line_width < 1:
|
|
33
|
+
line_width = 1
|
|
34
|
+
pl = Polyline(
|
|
35
|
+
id=_generate_id(), points=list(points), curved=curved, closed=closed,
|
|
36
|
+
xloc=xloc, line_color=line_color, fill_color=fill_color,
|
|
37
|
+
line_style=line_style, line_width=line_width, force_overlay=force_overlay,
|
|
38
|
+
)
|
|
39
|
+
_all_polylines.append(pl)
|
|
40
|
+
return pl
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def delete(id: Polyline) -> None:
|
|
44
|
+
for i, p in enumerate(_all_polylines):
|
|
45
|
+
if p.id == id.id:
|
|
46
|
+
_all_polylines.pop(i)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_all() -> list[Polyline]:
|
|
51
|
+
return list(_all_polylines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def clear_all() -> None:
|
|
55
|
+
_all_polylines.clear()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def reset_id_counter() -> None:
|
|
59
|
+
global _polyline_id_counter
|
|
60
|
+
_polyline_id_counter = 0
|
oakscriptpy/runtime.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Runtime module — global context management, plot, and hline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from .runtime_types import OakScriptContext
|
|
10
|
+
|
|
11
|
+
_context: OakScriptContext | None = None
|
|
12
|
+
_calculate_fn: Callable[[], None] | None = None
|
|
13
|
+
_active_plots: list[dict[str, Any]] = []
|
|
14
|
+
_plot_counter: int = 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_context(ctx: OakScriptContext) -> None:
|
|
18
|
+
global _context, _plot_counter
|
|
19
|
+
_context = ctx
|
|
20
|
+
_plot_counter = 0
|
|
21
|
+
clear_plots()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clear_context() -> None:
|
|
25
|
+
global _context, _calculate_fn, _plot_counter
|
|
26
|
+
clear_plots()
|
|
27
|
+
_context = None
|
|
28
|
+
_calculate_fn = None
|
|
29
|
+
_plot_counter = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_context() -> OakScriptContext | None:
|
|
33
|
+
return _context
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def register_calculate(fn: Callable[[], None]) -> None:
|
|
37
|
+
global _calculate_fn
|
|
38
|
+
_calculate_fn = fn
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def recalculate() -> None:
|
|
42
|
+
global _plot_counter
|
|
43
|
+
if _calculate_fn is not None:
|
|
44
|
+
clear_plots()
|
|
45
|
+
_plot_counter = 0
|
|
46
|
+
_calculate_fn()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def clear_plots() -> None:
|
|
50
|
+
if _context is not None:
|
|
51
|
+
for p in _active_plots:
|
|
52
|
+
try:
|
|
53
|
+
_context.chart.remove_series(p["series"])
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
_active_plots.clear()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_series_type(style: str | None) -> str:
|
|
60
|
+
if style in ("histogram", "columns"):
|
|
61
|
+
return "histogram"
|
|
62
|
+
if style in ("area", "areabr"):
|
|
63
|
+
return "area"
|
|
64
|
+
return "line"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_line_style(style: str | None) -> int:
|
|
68
|
+
if style == "dotted":
|
|
69
|
+
return 1
|
|
70
|
+
if style == "dashed":
|
|
71
|
+
return 2
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def plot(
|
|
76
|
+
series: list[float],
|
|
77
|
+
title: str | None = None,
|
|
78
|
+
color: str | None = None,
|
|
79
|
+
linewidth: int | None = None,
|
|
80
|
+
style: str | None = None,
|
|
81
|
+
colors: list[str] | None = None,
|
|
82
|
+
) -> str:
|
|
83
|
+
global _plot_counter
|
|
84
|
+
if _context is None:
|
|
85
|
+
raise RuntimeError("OakScript context not set. Call set_context() before plotting.")
|
|
86
|
+
|
|
87
|
+
suffix = ""
|
|
88
|
+
if title:
|
|
89
|
+
suffix = "_" + re.sub(r"\s+", "_", title)
|
|
90
|
+
plot_id = f"plot_{_plot_counter}{suffix}"
|
|
91
|
+
_plot_counter += 1
|
|
92
|
+
|
|
93
|
+
series_type = _get_series_type(style)
|
|
94
|
+
options: dict[str, Any] = {}
|
|
95
|
+
if color is not None:
|
|
96
|
+
options["color"] = color
|
|
97
|
+
if linewidth is not None:
|
|
98
|
+
options["lineWidth"] = linewidth
|
|
99
|
+
options["lineStyle"] = _get_line_style(style)
|
|
100
|
+
|
|
101
|
+
series_handle = _context.chart.add_series(series_type, options)
|
|
102
|
+
|
|
103
|
+
data: list[dict[str, Any]] = []
|
|
104
|
+
times = _context.ohlcv.time
|
|
105
|
+
for i, value in enumerate(series):
|
|
106
|
+
if i < len(times) and not math.isnan(value):
|
|
107
|
+
point: dict[str, Any] = {"time": times[i], "value": value}
|
|
108
|
+
if colors and i < len(colors) and colors[i]:
|
|
109
|
+
point["color"] = colors[i]
|
|
110
|
+
data.append(point)
|
|
111
|
+
|
|
112
|
+
series_handle.set_data(data)
|
|
113
|
+
_active_plots.append({"id": plot_id, "series": series_handle})
|
|
114
|
+
return plot_id
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def hline(
|
|
118
|
+
price: float,
|
|
119
|
+
title: str | None = None,
|
|
120
|
+
color: str | None = None,
|
|
121
|
+
linestyle: str | None = None,
|
|
122
|
+
linewidth: int | None = None,
|
|
123
|
+
) -> str:
|
|
124
|
+
global _plot_counter
|
|
125
|
+
if _context is None:
|
|
126
|
+
raise RuntimeError("OakScript context not set. Call set_context() before creating hlines.")
|
|
127
|
+
|
|
128
|
+
suffix = ""
|
|
129
|
+
if title:
|
|
130
|
+
suffix = "_" + re.sub(r"\s+", "_", title)
|
|
131
|
+
hline_id = f"hline_{_plot_counter}{suffix}"
|
|
132
|
+
_plot_counter += 1
|
|
133
|
+
|
|
134
|
+
options: dict[str, Any] = {}
|
|
135
|
+
if color is not None:
|
|
136
|
+
options["color"] = color
|
|
137
|
+
if linewidth is not None:
|
|
138
|
+
options["lineWidth"] = linewidth
|
|
139
|
+
options["lineStyle"] = _get_line_style(linestyle)
|
|
140
|
+
|
|
141
|
+
series_handle = _context.chart.add_series("line", options)
|
|
142
|
+
data = [{"time": t, "value": price} for t in _context.ohlcv.time]
|
|
143
|
+
series_handle.set_data(data)
|
|
144
|
+
|
|
145
|
+
_active_plots.append({"id": hline_id, "series": series_handle})
|
|
146
|
+
return hline_id
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_active_plots() -> list[dict[str, Any]]:
|
|
150
|
+
return list(_active_plots)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Runtime type definitions — Protocol classes for adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SeriesHandle(Protocol):
|
|
10
|
+
def set_data(self, data: list[dict[str, Any]]) -> None: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChartAdapter(Protocol):
|
|
14
|
+
def add_series(self, type: str, options: dict[str, Any] | None = None) -> SeriesHandle: ...
|
|
15
|
+
def remove_series(self, series: SeriesHandle) -> None: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class InputConfig:
|
|
20
|
+
id: str
|
|
21
|
+
type: str # 'int' | 'float' | 'bool' | 'string' | 'source' | 'color'
|
|
22
|
+
defval: Any
|
|
23
|
+
title: str | None = None
|
|
24
|
+
min: float | None = None
|
|
25
|
+
max: float | None = None
|
|
26
|
+
step: float | None = None
|
|
27
|
+
options: list[str] | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InputAdapter(Protocol):
|
|
31
|
+
def register_input(self, config: InputConfig) -> Any: ...
|
|
32
|
+
def get_value(self, id: str) -> Any: ...
|
|
33
|
+
def set_value(self, id: str, value: Any) -> None: ...
|
|
34
|
+
def on_input_change(self, callback: Callable[[str, Any], None]) -> None: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class OhlcvData:
|
|
39
|
+
time: list[int]
|
|
40
|
+
open: list[float]
|
|
41
|
+
high: list[float]
|
|
42
|
+
low: list[float]
|
|
43
|
+
close: list[float]
|
|
44
|
+
volume: list[float]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class OakScriptContext:
|
|
49
|
+
chart: Any # ChartAdapter
|
|
50
|
+
inputs: Any # InputAdapter
|
|
51
|
+
ohlcv: OhlcvData
|
|
52
|
+
bar_index: int = 0
|
oakscriptpy/series.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Series + BarData classes — lazy time-series computation with cache invalidation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from ._types import Bar
|
|
9
|
+
|
|
10
|
+
SeriesExtractor = Callable[[Bar, int, list[Bar]], float]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BarData:
|
|
14
|
+
"""Versioned wrapper around list[Bar] for cache invalidation.
|
|
15
|
+
|
|
16
|
+
Tracks a version number that increments whenever the data is mutated.
|
|
17
|
+
Series objects reference this to detect when cached values need recomputation.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("_bars", "_version")
|
|
21
|
+
|
|
22
|
+
def __init__(self, bars: list[Bar] | None = None) -> None:
|
|
23
|
+
self._bars: list[Bar] = bars if bars is not None else []
|
|
24
|
+
self._version: int = 0
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def version(self) -> int:
|
|
28
|
+
return self._version
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def bars(self) -> list[Bar]:
|
|
32
|
+
return self._bars
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def length(self) -> int:
|
|
36
|
+
return len(self._bars)
|
|
37
|
+
|
|
38
|
+
def push(self, bar: Bar) -> None:
|
|
39
|
+
self._bars.append(bar)
|
|
40
|
+
self._version += 1
|
|
41
|
+
|
|
42
|
+
def pop(self) -> Bar | None:
|
|
43
|
+
if self._bars:
|
|
44
|
+
bar = self._bars.pop()
|
|
45
|
+
self._version += 1
|
|
46
|
+
return bar
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def set(self, index: int, bar: Bar) -> None:
|
|
50
|
+
if 0 <= index < len(self._bars):
|
|
51
|
+
self._bars[index] = bar
|
|
52
|
+
self._version += 1
|
|
53
|
+
|
|
54
|
+
def update_last(self, bar: Bar) -> None:
|
|
55
|
+
if self._bars:
|
|
56
|
+
self._bars[-1] = bar
|
|
57
|
+
self._version += 1
|
|
58
|
+
|
|
59
|
+
def set_all(self, bars: list[Bar]) -> None:
|
|
60
|
+
self._bars = bars
|
|
61
|
+
self._version += 1
|
|
62
|
+
|
|
63
|
+
def invalidate(self) -> None:
|
|
64
|
+
self._version += 1
|
|
65
|
+
|
|
66
|
+
def at(self, index: int) -> Bar | None:
|
|
67
|
+
if 0 <= index < len(self._bars):
|
|
68
|
+
return self._bars[index]
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def from_bars(bars: list[Bar]) -> BarData:
|
|
73
|
+
return BarData(bars)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Series:
|
|
77
|
+
"""Lazy time-series with operator overloading and automatic cache invalidation.
|
|
78
|
+
|
|
79
|
+
Operations on Series return new Series with composed extractors.
|
|
80
|
+
Values are only computed when to_array() or get() is called.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
__slots__ = ("_extractor", "_data_source", "_cached", "_cached_version")
|
|
84
|
+
|
|
85
|
+
def __init__(self, data: list[Bar] | BarData, extractor: SeriesExtractor) -> None:
|
|
86
|
+
self._data_source: BarData = data if isinstance(data, BarData) else BarData(data)
|
|
87
|
+
self._extractor: SeriesExtractor = extractor
|
|
88
|
+
self._cached: list[float] | None = None
|
|
89
|
+
self._cached_version: int = -1
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def bars(self) -> list[Bar]:
|
|
93
|
+
return self._data_source.bars
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def bar_data(self) -> BarData:
|
|
97
|
+
return self._data_source
|
|
98
|
+
|
|
99
|
+
# --- Factory Methods ---
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def from_bars(bars: list[Bar] | BarData, field: str) -> Series:
|
|
103
|
+
def extractor(bar: Bar, _i: int, _data: list[Bar]) -> float:
|
|
104
|
+
v = getattr(bar, field, None)
|
|
105
|
+
return float("nan") if v is None else float(v)
|
|
106
|
+
return Series(bars, extractor)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def constant(bars: list[Bar] | BarData, value: float) -> Series:
|
|
110
|
+
return Series(bars, lambda _b, _i, _d: value)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def from_array(bars: list[Bar] | BarData, values: list[float]) -> Series:
|
|
114
|
+
def extractor(_b: Bar, i: int, _d: list[Bar]) -> float:
|
|
115
|
+
return values[i] if i < len(values) else float("nan")
|
|
116
|
+
return Series(bars, extractor)
|
|
117
|
+
|
|
118
|
+
# --- Arithmetic (Python operator overloading) ---
|
|
119
|
+
|
|
120
|
+
def _binop(self, other: Series | float | int, op: Callable[[float, float], float]) -> Series:
|
|
121
|
+
if isinstance(other, Series):
|
|
122
|
+
ext = self._extractor
|
|
123
|
+
other_ext = other._extractor
|
|
124
|
+
return Series(self._data_source, lambda b, i, d: op(ext(b, i, d), other_ext(b, i, d)))
|
|
125
|
+
else:
|
|
126
|
+
val = float(other)
|
|
127
|
+
ext = self._extractor
|
|
128
|
+
return Series(self._data_source, lambda b, i, d: op(ext(b, i, d), val))
|
|
129
|
+
|
|
130
|
+
def _rbinop(self, other: float | int, op: Callable[[float, float], float]) -> Series:
|
|
131
|
+
val = float(other)
|
|
132
|
+
ext = self._extractor
|
|
133
|
+
return Series(self._data_source, lambda b, i, d: op(val, ext(b, i, d)))
|
|
134
|
+
|
|
135
|
+
def __add__(self, other: Series | float | int) -> Series:
|
|
136
|
+
return self._binop(other, lambda a, b: a + b)
|
|
137
|
+
|
|
138
|
+
def __radd__(self, other: float | int) -> Series:
|
|
139
|
+
return self._rbinop(other, lambda a, b: a + b)
|
|
140
|
+
|
|
141
|
+
def __sub__(self, other: Series | float | int) -> Series:
|
|
142
|
+
return self._binop(other, lambda a, b: a - b)
|
|
143
|
+
|
|
144
|
+
def __rsub__(self, other: float | int) -> Series:
|
|
145
|
+
return self._rbinop(other, lambda a, b: a - b)
|
|
146
|
+
|
|
147
|
+
def __mul__(self, other: Series | float | int) -> Series:
|
|
148
|
+
return self._binop(other, lambda a, b: a * b)
|
|
149
|
+
|
|
150
|
+
def __rmul__(self, other: float | int) -> Series:
|
|
151
|
+
return self._rbinop(other, lambda a, b: a * b)
|
|
152
|
+
|
|
153
|
+
def __truediv__(self, other: Series | float | int) -> Series:
|
|
154
|
+
return self._binop(other, lambda a, b: a / b if b != 0 else float("nan"))
|
|
155
|
+
|
|
156
|
+
def __rtruediv__(self, other: float | int) -> Series:
|
|
157
|
+
return self._rbinop(other, lambda a, b: a / b if b != 0 else float("nan"))
|
|
158
|
+
|
|
159
|
+
def __mod__(self, other: Series | float | int) -> Series:
|
|
160
|
+
return self._binop(other, lambda a, b: a % b if b != 0 else float("nan"))
|
|
161
|
+
|
|
162
|
+
def __rmod__(self, other: float | int) -> Series:
|
|
163
|
+
return self._rbinop(other, lambda a, b: a % b if b != 0 else float("nan"))
|
|
164
|
+
|
|
165
|
+
def __neg__(self) -> Series:
|
|
166
|
+
ext = self._extractor
|
|
167
|
+
return Series(self._data_source, lambda b, i, d: -ext(b, i, d))
|
|
168
|
+
|
|
169
|
+
def __abs__(self) -> Series:
|
|
170
|
+
ext = self._extractor
|
|
171
|
+
return Series(self._data_source, lambda b, i, d: abs(ext(b, i, d)))
|
|
172
|
+
|
|
173
|
+
# --- Comparison (return Series with 1/0) ---
|
|
174
|
+
|
|
175
|
+
def __gt__(self, other: Series | float | int) -> Series: # type: ignore[override]
|
|
176
|
+
return self._binop(other, lambda a, b: 1.0 if a > b else 0.0)
|
|
177
|
+
|
|
178
|
+
def __ge__(self, other: Series | float | int) -> Series: # type: ignore[override]
|
|
179
|
+
return self._binop(other, lambda a, b: 1.0 if a >= b else 0.0)
|
|
180
|
+
|
|
181
|
+
def __lt__(self, other: Series | float | int) -> Series: # type: ignore[override]
|
|
182
|
+
return self._binop(other, lambda a, b: 1.0 if a < b else 0.0)
|
|
183
|
+
|
|
184
|
+
def __le__(self, other: Series | float | int) -> Series: # type: ignore[override]
|
|
185
|
+
return self._binop(other, lambda a, b: 1.0 if a <= b else 0.0)
|
|
186
|
+
|
|
187
|
+
def eq(self, other: Series | float | int) -> Series:
|
|
188
|
+
return self._binop(other, lambda a, b: 1.0 if a == b else 0.0)
|
|
189
|
+
|
|
190
|
+
def neq(self, other: Series | float | int) -> Series:
|
|
191
|
+
return self._binop(other, lambda a, b: 1.0 if a != b else 0.0)
|
|
192
|
+
|
|
193
|
+
# --- Logical (keywords in Python, so use methods) ---
|
|
194
|
+
|
|
195
|
+
def and_(self, other: Series | float | int) -> Series:
|
|
196
|
+
if isinstance(other, Series):
|
|
197
|
+
ext = self._extractor
|
|
198
|
+
other_ext = other._extractor
|
|
199
|
+
return Series(self._data_source, lambda b, i, d: 1.0 if ext(b, i, d) and other_ext(b, i, d) else 0.0)
|
|
200
|
+
else:
|
|
201
|
+
val = float(other)
|
|
202
|
+
ext = self._extractor
|
|
203
|
+
return Series(self._data_source, lambda b, i, d: 1.0 if ext(b, i, d) and val else 0.0)
|
|
204
|
+
|
|
205
|
+
def or_(self, other: Series | float | int) -> Series:
|
|
206
|
+
if isinstance(other, Series):
|
|
207
|
+
ext = self._extractor
|
|
208
|
+
other_ext = other._extractor
|
|
209
|
+
return Series(self._data_source, lambda b, i, d: 1.0 if ext(b, i, d) or other_ext(b, i, d) else 0.0)
|
|
210
|
+
else:
|
|
211
|
+
val = float(other)
|
|
212
|
+
ext = self._extractor
|
|
213
|
+
return Series(self._data_source, lambda b, i, d: 1.0 if ext(b, i, d) or val else 0.0)
|
|
214
|
+
|
|
215
|
+
def not_(self) -> Series:
|
|
216
|
+
ext = self._extractor
|
|
217
|
+
return Series(self._data_source, lambda b, i, d: 1.0 if not ext(b, i, d) else 0.0)
|
|
218
|
+
|
|
219
|
+
# --- Conditional Selection ---
|
|
220
|
+
|
|
221
|
+
def iff(self, true_value: Series | float | int, false_value: Series | float | int) -> Series:
|
|
222
|
+
"""Bar-by-bar conditional selection (ternary operator for Series)."""
|
|
223
|
+
ext = self._extractor
|
|
224
|
+
tv_is_series = isinstance(true_value, Series)
|
|
225
|
+
fv_is_series = isinstance(false_value, Series)
|
|
226
|
+
tv_ext = true_value._extractor if tv_is_series else None
|
|
227
|
+
fv_ext = false_value._extractor if fv_is_series else None
|
|
228
|
+
tv_num = 0.0 if tv_is_series else float(true_value)
|
|
229
|
+
fv_num = 0.0 if fv_is_series else float(false_value)
|
|
230
|
+
|
|
231
|
+
def _iff(b: Bar, i: int, d: list[Bar]) -> float:
|
|
232
|
+
cond = ext(b, i, d)
|
|
233
|
+
is_truthy = cond != 0 and not math.isnan(cond)
|
|
234
|
+
if is_truthy:
|
|
235
|
+
return tv_ext(b, i, d) if tv_ext is not None else tv_num
|
|
236
|
+
else:
|
|
237
|
+
return fv_ext(b, i, d) if fv_ext is not None else fv_num
|
|
238
|
+
|
|
239
|
+
return Series(self._data_source, _iff)
|
|
240
|
+
|
|
241
|
+
# --- Offset/History ---
|
|
242
|
+
|
|
243
|
+
def offset(self, n: int) -> Series:
|
|
244
|
+
"""Access previous bars (like close[1] in PineScript)."""
|
|
245
|
+
ext = self._extractor
|
|
246
|
+
|
|
247
|
+
def _offset(_b: Bar, i: int, d: list[Bar]) -> float:
|
|
248
|
+
target = i - n
|
|
249
|
+
if target < 0 or target >= len(d):
|
|
250
|
+
return float("nan")
|
|
251
|
+
return ext(d[target], target, d)
|
|
252
|
+
|
|
253
|
+
return Series(self._data_source, _offset)
|
|
254
|
+
|
|
255
|
+
# --- Computation & Access ---
|
|
256
|
+
|
|
257
|
+
def to_array(self) -> list[float]:
|
|
258
|
+
if self._cached is not None and self._cached_version == self._data_source.version:
|
|
259
|
+
return self._cached
|
|
260
|
+
bars = self._data_source.bars
|
|
261
|
+
self._cached = [self._extractor(bar, i, bars) for i, bar in enumerate(bars)]
|
|
262
|
+
self._cached_version = self._data_source.version
|
|
263
|
+
return self._cached
|
|
264
|
+
|
|
265
|
+
def materialize(self) -> Series:
|
|
266
|
+
"""Eagerly compute values and break closure chain for memory efficiency."""
|
|
267
|
+
values = list(self.to_array())
|
|
268
|
+
return Series.from_array(self._data_source, values)
|
|
269
|
+
|
|
270
|
+
def _invalidate(self) -> None:
|
|
271
|
+
self._cached = None
|
|
272
|
+
self._cached_version = -1
|
|
273
|
+
|
|
274
|
+
def get(self, index: int) -> float:
|
|
275
|
+
values = self.to_array()
|
|
276
|
+
return values[index] if 0 <= index < len(values) else float("nan")
|
|
277
|
+
|
|
278
|
+
def last(self) -> float:
|
|
279
|
+
values = self.to_array()
|
|
280
|
+
return values[-1] if values else float("nan")
|
|
281
|
+
|
|
282
|
+
def length(self) -> int:
|
|
283
|
+
return self._data_source.length
|
|
284
|
+
|
|
285
|
+
def to_time_value_pairs(self) -> list[dict[str, Any]]:
|
|
286
|
+
values = self.to_array()
|
|
287
|
+
bars = self._data_source.bars
|
|
288
|
+
return [
|
|
289
|
+
{"time": bar.time, "value": v}
|
|
290
|
+
for bar, v in zip(bars, values)
|
|
291
|
+
if not math.isnan(v)
|
|
292
|
+
]
|