oakscriptpy 0.1.4__tar.gz → 0.2.0__tar.gz
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-0.1.4 → oakscriptpy-0.2.0}/DEVLOG.md +32 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/PKG-INFO +65 -30
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/README.md +64 -29
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/pyproject.toml +1 -1
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/__init__.py +4 -4
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/_types.py +21 -1
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/_utils.py +42 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/indicator.py +64 -4
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/series.py +47 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/.github/workflows/publish.yml +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/.gitignore +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/logo.png +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/_metadata.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/adapters/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/adapters/simple_input.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/array.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/box.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/chartpoint.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/color.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/input_.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/inputs.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/label.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/lib/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/lib/zigzag.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/line.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/linefill.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/math_.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/matrix.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/plot_.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/polyline.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/runtime.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/runtime_types.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/str_.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/ta.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/ta_series.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/time_.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_drawing_objects.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_final_functions.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_new_functions.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_percentile.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_search_and_stats.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/box/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/box/test_box.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/chartpoint/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/chartpoint/test_chartpoint.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_components.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_constants.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_creation.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_manipulation.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/conftest.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/indicator/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/indicator/test_indicator.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/label/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/label/test_label.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/line/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/line/test_line.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/linefill/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/linefill/test_linefill.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_algebraic.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_basic.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_series_support.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_sum_series.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_trigonometric.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_utility.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/test_foundational.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/test_linear_algebra.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/test_manipulation.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/polyline/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/polyline/test_polyline.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_adapters.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_inputs.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_runtime.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_series.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_conversion.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_formatting.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_manipulation.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_predicates.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_search.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_whitespace.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/__init__.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_ichimoku.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_functions.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_functions2.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_functions3.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_ta_functions.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_rsi.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_sma.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_supertrend.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_ta_series_integration.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_zigzag.py +0 -0
- {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/test_new_functions.py +0 -0
|
@@ -79,3 +79,35 @@ Port all Jest test files from oakscriptJS to Python pytest equivalents for math,
|
|
|
79
79
|
- Python `str.split('', '')` raises ValueError (tested as such instead of returning `[]`)
|
|
80
80
|
- Python `substring` uses direct slicing (negative indices and begin>end behave differently from JS)
|
|
81
81
|
- Python counts Unicode code points, not UTF-16 units (emoji = 1 char, not 2)
|
|
82
|
+
|
|
83
|
+
## 2026-02-28 - PineScript DSL ergonomics (#10, #11, #12, #16)
|
|
84
|
+
|
|
85
|
+
### Goal
|
|
86
|
+
Make the Python API feel closer to PineScript's DSL by reducing boilerplate and adding familiar patterns.
|
|
87
|
+
|
|
88
|
+
### Approach Taken
|
|
89
|
+
Reviewed PineScript v5/v6 DSL characteristics against the current API. Identified achievable ergonomic wins vs inherent Python limitations. Implemented four features:
|
|
90
|
+
|
|
91
|
+
1. `Series.__getitem__` — `close[1]` instead of `close.offset(1)`
|
|
92
|
+
2. `BuiltIns` class — pre-built OHLCV Series from bar data
|
|
93
|
+
3. `na` first-class concept — enhanced `na` constant, Series-aware `nz()` and `fixnan()`
|
|
94
|
+
4. Decorator-style `@indicator()` — minimal working program pattern
|
|
95
|
+
|
|
96
|
+
### What Worked
|
|
97
|
+
- `__getitem__` was trivial (delegates to `offset()`, returns self for `[0]`)
|
|
98
|
+
- `BuiltIns` as a simple class with pre-built Series avoids proxy/magic complexity
|
|
99
|
+
- `na.__float__()` lets it participate in arithmetic as NaN
|
|
100
|
+
- `indicator()` overload detects `str` vs `IndicatorMetadataConfig` for backward-compatible dual API
|
|
101
|
+
|
|
102
|
+
### What Failed
|
|
103
|
+
- Cannot override `and`/`or`/`not` or `==`/`!=` on Series — Python language limitation. Documented as known gaps (#13, #14)
|
|
104
|
+
|
|
105
|
+
### Current State
|
|
106
|
+
- **Done**: All 4 features shipped, 1224 tests passing
|
|
107
|
+
- **Remaining open issues**: #13 (logical operators, wontfix), #14 (equality operators, wontfix), #15 (missing namespaces, future)
|
|
108
|
+
|
|
109
|
+
### Key Decisions
|
|
110
|
+
- `BuiltIns` is an explicit object (not module-level proxies) — no thread-local state, no import magic
|
|
111
|
+
- `close[n]` only accepts non-negative integers, raises `ValueError` otherwise
|
|
112
|
+
- `@indicator("title")` returns `Script` with `.calculate(bars)`, legacy API unchanged
|
|
113
|
+
- `nz()`/`fixnan()` on Series create new lazy Series (no eager computation)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oakscriptpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: PineScript-like API for technical analysis in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/deepentropy/oakscriptPy
|
|
6
6
|
Project-URL: Repository, https://github.com/deepentropy/oakscriptPy
|
|
@@ -40,33 +40,66 @@ pip install oakscriptpy
|
|
|
40
40
|
## Quick Start
|
|
41
41
|
|
|
42
42
|
```python
|
|
43
|
-
from oakscriptpy import
|
|
43
|
+
from oakscriptpy import indicator, ta, nz
|
|
44
|
+
|
|
45
|
+
@indicator("My SMA", overlay=True)
|
|
46
|
+
def my_sma(b):
|
|
47
|
+
sma14 = ta.sma(b.close, 14)
|
|
48
|
+
return nz(sma14)
|
|
49
|
+
|
|
50
|
+
# Run on bar data
|
|
44
51
|
from oakscriptpy._types import Bar
|
|
52
|
+
bars = [Bar(time=1, open=10, high=12, low=9, close=11, volume=100), ...]
|
|
53
|
+
result = my_sma.calculate(bars)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The `@indicator` decorator provides a `BuiltIns` object with pre-built Series for all standard fields:
|
|
57
|
+
|
|
58
|
+
| Variable | Description |
|
|
59
|
+
|----------|-------------|
|
|
60
|
+
| `b.open`, `b.high`, `b.low`, `b.close`, `b.volume` | OHLCV price data |
|
|
61
|
+
| `b.hl2`, `b.hlc3`, `b.ohlc4` | Derived composites |
|
|
62
|
+
| `b.bar_index`, `b.time` | Bar identity |
|
|
63
|
+
|
|
64
|
+
## PineScript-like Syntax
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from oakscriptpy import BuiltIns, ta, nz, fixnan, na
|
|
68
|
+
|
|
69
|
+
b = BuiltIns(bars)
|
|
45
70
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
# History access: close[1] instead of close.offset(1)
|
|
72
|
+
change = b.close - b.close[1]
|
|
73
|
+
|
|
74
|
+
# TA functions
|
|
75
|
+
sma = ta.sma(b.close, 14)
|
|
76
|
+
rsi = ta.rsi(b.close, 14)
|
|
77
|
+
macd_line, signal, hist = ta.macd(b.close, 12, 26, 9)
|
|
78
|
+
|
|
79
|
+
# na handling
|
|
80
|
+
safe = nz(sma) # replace NaN with 0
|
|
81
|
+
filled = fixnan(sma) # forward-fill NaN values
|
|
82
|
+
if na(sma.last()):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Comparisons and logic
|
|
86
|
+
bullish = b.close > b.open
|
|
87
|
+
above_sma = b.close > sma
|
|
88
|
+
signal = bullish.and_(above_sma)
|
|
89
|
+
result = signal.iff(1, -1)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Array-based API
|
|
93
|
+
|
|
94
|
+
For direct computation without Series overhead:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from oakscriptpy import ta as ta_core
|
|
53
98
|
|
|
54
|
-
# Array-based TA
|
|
55
99
|
closes = [b.close for b in bars]
|
|
56
|
-
sma_values =
|
|
57
|
-
ema_values =
|
|
58
|
-
rsi_values =
|
|
59
|
-
|
|
60
|
-
# Series-based TA (with operator overloading)
|
|
61
|
-
from oakscriptpy import ta as ta_series
|
|
62
|
-
close_series = Series.from_bars(bars, lambda b: b.close)
|
|
63
|
-
sma_series = ta_series.sma(close_series, 3)
|
|
64
|
-
ema_series = ta_series.ema(close_series, 3)
|
|
65
|
-
|
|
66
|
-
# Series arithmetic
|
|
67
|
-
spread = close_series - sma_series
|
|
68
|
-
doubled = close_series * 2
|
|
69
|
-
above_sma = close_series > sma_series
|
|
100
|
+
sma_values = ta_core.sma(closes, 3) # list[float]
|
|
101
|
+
ema_values = ta_core.ema(closes, 3)
|
|
102
|
+
rsi_values = ta_core.rsi(closes, 14)
|
|
70
103
|
```
|
|
71
104
|
|
|
72
105
|
## Namespaces
|
|
@@ -92,20 +125,22 @@ above_sma = close_series > sma_series
|
|
|
92
125
|
The `Series` class provides lazy evaluation with Python operator overloading:
|
|
93
126
|
|
|
94
127
|
```python
|
|
95
|
-
|
|
96
|
-
b = Series.from_bars(bars, lambda b: b.open)
|
|
128
|
+
b = BuiltIns(bars)
|
|
97
129
|
|
|
98
130
|
# Arithmetic: +, -, *, /, %
|
|
99
|
-
spread =
|
|
131
|
+
spread = b.close - b.open
|
|
100
132
|
|
|
101
133
|
# Comparison: >, >=, <, <=, eq(), neq()
|
|
102
|
-
bullish =
|
|
134
|
+
bullish = b.close > b.open
|
|
103
135
|
|
|
104
136
|
# Logical: and_(), or_(), not_()
|
|
105
|
-
signal = bullish.and_(
|
|
137
|
+
signal = bullish.and_(b.close > sma)
|
|
106
138
|
|
|
107
139
|
# History access
|
|
108
|
-
prev_close =
|
|
140
|
+
prev_close = b.close[1]
|
|
141
|
+
|
|
142
|
+
# Conditional (ternary)
|
|
143
|
+
result = bullish.iff(1, -1)
|
|
109
144
|
```
|
|
110
145
|
|
|
111
146
|
## Tests
|
|
@@ -19,33 +19,66 @@ pip install oakscriptpy
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
21
21
|
```python
|
|
22
|
-
from oakscriptpy import
|
|
22
|
+
from oakscriptpy import indicator, ta, nz
|
|
23
|
+
|
|
24
|
+
@indicator("My SMA", overlay=True)
|
|
25
|
+
def my_sma(b):
|
|
26
|
+
sma14 = ta.sma(b.close, 14)
|
|
27
|
+
return nz(sma14)
|
|
28
|
+
|
|
29
|
+
# Run on bar data
|
|
23
30
|
from oakscriptpy._types import Bar
|
|
31
|
+
bars = [Bar(time=1, open=10, high=12, low=9, close=11, volume=100), ...]
|
|
32
|
+
result = my_sma.calculate(bars)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The `@indicator` decorator provides a `BuiltIns` object with pre-built Series for all standard fields:
|
|
36
|
+
|
|
37
|
+
| Variable | Description |
|
|
38
|
+
|----------|-------------|
|
|
39
|
+
| `b.open`, `b.high`, `b.low`, `b.close`, `b.volume` | OHLCV price data |
|
|
40
|
+
| `b.hl2`, `b.hlc3`, `b.ohlc4` | Derived composites |
|
|
41
|
+
| `b.bar_index`, `b.time` | Bar identity |
|
|
42
|
+
|
|
43
|
+
## PineScript-like Syntax
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from oakscriptpy import BuiltIns, ta, nz, fixnan, na
|
|
47
|
+
|
|
48
|
+
b = BuiltIns(bars)
|
|
24
49
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
# History access: close[1] instead of close.offset(1)
|
|
51
|
+
change = b.close - b.close[1]
|
|
52
|
+
|
|
53
|
+
# TA functions
|
|
54
|
+
sma = ta.sma(b.close, 14)
|
|
55
|
+
rsi = ta.rsi(b.close, 14)
|
|
56
|
+
macd_line, signal, hist = ta.macd(b.close, 12, 26, 9)
|
|
57
|
+
|
|
58
|
+
# na handling
|
|
59
|
+
safe = nz(sma) # replace NaN with 0
|
|
60
|
+
filled = fixnan(sma) # forward-fill NaN values
|
|
61
|
+
if na(sma.last()):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Comparisons and logic
|
|
65
|
+
bullish = b.close > b.open
|
|
66
|
+
above_sma = b.close > sma
|
|
67
|
+
signal = bullish.and_(above_sma)
|
|
68
|
+
result = signal.iff(1, -1)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Array-based API
|
|
72
|
+
|
|
73
|
+
For direct computation without Series overhead:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from oakscriptpy import ta as ta_core
|
|
32
77
|
|
|
33
|
-
# Array-based TA
|
|
34
78
|
closes = [b.close for b in bars]
|
|
35
|
-
sma_values =
|
|
36
|
-
ema_values =
|
|
37
|
-
rsi_values =
|
|
38
|
-
|
|
39
|
-
# Series-based TA (with operator overloading)
|
|
40
|
-
from oakscriptpy import ta as ta_series
|
|
41
|
-
close_series = Series.from_bars(bars, lambda b: b.close)
|
|
42
|
-
sma_series = ta_series.sma(close_series, 3)
|
|
43
|
-
ema_series = ta_series.ema(close_series, 3)
|
|
44
|
-
|
|
45
|
-
# Series arithmetic
|
|
46
|
-
spread = close_series - sma_series
|
|
47
|
-
doubled = close_series * 2
|
|
48
|
-
above_sma = close_series > sma_series
|
|
79
|
+
sma_values = ta_core.sma(closes, 3) # list[float]
|
|
80
|
+
ema_values = ta_core.ema(closes, 3)
|
|
81
|
+
rsi_values = ta_core.rsi(closes, 14)
|
|
49
82
|
```
|
|
50
83
|
|
|
51
84
|
## Namespaces
|
|
@@ -71,20 +104,22 @@ above_sma = close_series > sma_series
|
|
|
71
104
|
The `Series` class provides lazy evaluation with Python operator overloading:
|
|
72
105
|
|
|
73
106
|
```python
|
|
74
|
-
|
|
75
|
-
b = Series.from_bars(bars, lambda b: b.open)
|
|
107
|
+
b = BuiltIns(bars)
|
|
76
108
|
|
|
77
109
|
# Arithmetic: +, -, *, /, %
|
|
78
|
-
spread =
|
|
110
|
+
spread = b.close - b.open
|
|
79
111
|
|
|
80
112
|
# Comparison: >, >=, <, <=, eq(), neq()
|
|
81
|
-
bullish =
|
|
113
|
+
bullish = b.close > b.open
|
|
82
114
|
|
|
83
115
|
# Logical: and_(), or_(), not_()
|
|
84
|
-
signal = bullish.and_(
|
|
116
|
+
signal = bullish.and_(b.close > sma)
|
|
85
117
|
|
|
86
118
|
# History access
|
|
87
|
-
prev_close =
|
|
119
|
+
prev_close = b.close[1]
|
|
120
|
+
|
|
121
|
+
# Conditional (ternary)
|
|
122
|
+
result = bullish.iff(1, -1)
|
|
88
123
|
```
|
|
89
124
|
|
|
90
125
|
## Tests
|
|
@@ -19,7 +19,7 @@ from . import chartpoint as chart_point
|
|
|
19
19
|
from . import polyline
|
|
20
20
|
|
|
21
21
|
# Series class (self-contained, no context)
|
|
22
|
-
from .series import Series, BarData
|
|
22
|
+
from .series import Series, BarData, BuiltIns
|
|
23
23
|
|
|
24
24
|
# TA-Series namespace (Series-based wrappers)
|
|
25
25
|
from . import ta_series as ta
|
|
@@ -41,7 +41,7 @@ from ._metadata import (
|
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
# Utilities
|
|
44
|
-
from ._utils import is_na, nz, ohlc_from_bars, get_close, get_high, get_low, get_open, get_source
|
|
44
|
+
from ._utils import is_na, nz, fixnan, ohlc_from_bars, get_close, get_high, get_low, get_open, get_source
|
|
45
45
|
|
|
46
46
|
# Runtime
|
|
47
47
|
from .runtime import (
|
|
@@ -58,7 +58,7 @@ from .inputs import (
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
# Indicator infrastructure
|
|
61
|
-
from .indicator import indicator, IndicatorMetadataConfig, IndicatorContext, IndicatorInstance
|
|
61
|
+
from .indicator import indicator, Script, IndicatorMetadataConfig, IndicatorContext, IndicatorInstance
|
|
62
62
|
|
|
63
63
|
# Input helper
|
|
64
64
|
from . import input_ as input
|
|
@@ -78,7 +78,7 @@ def alertcondition(_condition: object = None, _title: str | None = None, _messag
|
|
|
78
78
|
pass
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
VERSION = "0.
|
|
81
|
+
VERSION = "0.2.0"
|
|
82
82
|
|
|
83
83
|
info = {
|
|
84
84
|
"name": "OakScriptPy",
|
|
@@ -19,7 +19,14 @@ import math as _math
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class _Na:
|
|
22
|
-
"""PineScript-compatible NA: falsy constant
|
|
22
|
+
"""PineScript-compatible NA: falsy constant, callable checker, numeric NaN.
|
|
23
|
+
|
|
24
|
+
Usage::
|
|
25
|
+
|
|
26
|
+
na # falsy sentinel, acts as float('nan') in arithmetic
|
|
27
|
+
na(value) # True if value is None or NaN
|
|
28
|
+
na + 1 # nan (propagates like NaN)
|
|
29
|
+
"""
|
|
23
30
|
|
|
24
31
|
def __call__(self, value: object) -> bool:
|
|
25
32
|
if value is None:
|
|
@@ -31,9 +38,22 @@ class _Na:
|
|
|
31
38
|
def __bool__(self) -> bool:
|
|
32
39
|
return False
|
|
33
40
|
|
|
41
|
+
def __float__(self) -> float:
|
|
42
|
+
return float("nan")
|
|
43
|
+
|
|
34
44
|
def __repr__(self) -> str:
|
|
35
45
|
return "na"
|
|
36
46
|
|
|
47
|
+
def __eq__(self, other: object) -> bool:
|
|
48
|
+
if isinstance(other, _Na):
|
|
49
|
+
return True
|
|
50
|
+
if isinstance(other, float) and _math.isnan(other):
|
|
51
|
+
return True
|
|
52
|
+
return NotImplemented
|
|
53
|
+
|
|
54
|
+
def __hash__(self) -> int:
|
|
55
|
+
return hash(float("nan"))
|
|
56
|
+
|
|
37
57
|
|
|
38
58
|
na = _Na()
|
|
39
59
|
|
|
@@ -32,9 +32,51 @@ def is_na(value: object) -> bool:
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def nz(value: object, default: object = 0) -> object:
|
|
35
|
+
"""Replace na with default. Works on scalars and Series.
|
|
36
|
+
|
|
37
|
+
Scalar: nz(float('nan')) -> 0, nz(float('nan'), -1) -> -1
|
|
38
|
+
Series: nz(series) -> new Series with na values replaced by default
|
|
39
|
+
"""
|
|
40
|
+
from .series import Series
|
|
41
|
+
|
|
42
|
+
if isinstance(value, Series):
|
|
43
|
+
replacement = float(default)
|
|
44
|
+
ext = value._extractor
|
|
45
|
+
|
|
46
|
+
def _nz(b: Bar, i: int, d: list[Bar]) -> float:
|
|
47
|
+
v = ext(b, i, d)
|
|
48
|
+
return replacement if (v is None or (isinstance(v, float) and math.isnan(v))) else v
|
|
49
|
+
|
|
50
|
+
return Series(value._data_source, _nz)
|
|
35
51
|
return default if is_na(value) else value
|
|
36
52
|
|
|
37
53
|
|
|
54
|
+
def fixnan(value: object) -> object:
|
|
55
|
+
"""Replace na with last non-na value (forward fill). Works on scalars and Series.
|
|
56
|
+
|
|
57
|
+
Scalar: fixnan(float('nan')) -> float('nan') (no history to fill from)
|
|
58
|
+
Series: fixnan(series) -> new Series with na forward-filled
|
|
59
|
+
"""
|
|
60
|
+
from .series import Series
|
|
61
|
+
|
|
62
|
+
if isinstance(value, Series):
|
|
63
|
+
ext = value._extractor
|
|
64
|
+
|
|
65
|
+
def _fixnan(_b: Bar, i: int, d: list[Bar]) -> float:
|
|
66
|
+
v = ext(d[i], i, d)
|
|
67
|
+
if not math.isnan(v):
|
|
68
|
+
return v
|
|
69
|
+
# Walk backwards to find last non-na value
|
|
70
|
+
for j in range(i - 1, -1, -1):
|
|
71
|
+
prev = ext(d[j], j, d)
|
|
72
|
+
if not math.isnan(prev):
|
|
73
|
+
return prev
|
|
74
|
+
return float("nan")
|
|
75
|
+
|
|
76
|
+
return Series(value._data_source, _fixnan)
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
38
80
|
def clamp(value: float, min_val: float, max_val: float) -> float:
|
|
39
81
|
return max(min_val, min(max_val, value))
|
|
40
82
|
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from typing import Any, Callable
|
|
7
7
|
|
|
8
8
|
from ._types import Bar
|
|
9
|
+
from .series import BuiltIns
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass
|
|
@@ -63,11 +64,70 @@ class IndicatorInstance:
|
|
|
63
64
|
self._setup(ctx)
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
class Script:
|
|
68
|
+
"""Decorator-style indicator: receives BuiltIns, returns computed results.
|
|
69
|
+
|
|
70
|
+
Usage::
|
|
71
|
+
|
|
72
|
+
@indicator("My SMA", overlay=True)
|
|
73
|
+
def my_sma(b):
|
|
74
|
+
return ta.sma(b.close, 14)
|
|
75
|
+
|
|
76
|
+
result = my_sma.calculate(bars)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, metadata: IndicatorMetadataConfig, fn: Callable[[BuiltIns], Any]) -> None:
|
|
80
|
+
self.metadata = metadata
|
|
81
|
+
self._fn = fn
|
|
82
|
+
|
|
83
|
+
def calculate(self, data: list[Bar]) -> Any:
|
|
84
|
+
b = BuiltIns(data)
|
|
85
|
+
return self._fn(b)
|
|
86
|
+
|
|
87
|
+
|
|
66
88
|
def indicator(
|
|
67
|
-
|
|
68
|
-
setup: Callable[[IndicatorContext], None],
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
title_or_metadata: str | IndicatorMetadataConfig,
|
|
90
|
+
setup: Callable[[IndicatorContext], None] | None = None,
|
|
91
|
+
/,
|
|
92
|
+
*,
|
|
93
|
+
overlay: bool = False,
|
|
94
|
+
short_title: str | None = None,
|
|
95
|
+
format: str = "price",
|
|
96
|
+
precision: int = 2,
|
|
97
|
+
) -> Callable[[], IndicatorInstance] | Callable[..., Script]:
|
|
98
|
+
"""Create an indicator — as a factory (legacy) or as a decorator (new).
|
|
99
|
+
|
|
100
|
+
Legacy API::
|
|
101
|
+
|
|
102
|
+
factory = indicator(IndicatorMetadataConfig(...), setup_fn)
|
|
103
|
+
instance = factory()
|
|
104
|
+
instance.calculate(bars)
|
|
105
|
+
|
|
106
|
+
Decorator API::
|
|
107
|
+
|
|
108
|
+
@indicator("My Script", overlay=True)
|
|
109
|
+
def my_script(b):
|
|
110
|
+
return ta.sma(b.close, 14)
|
|
111
|
+
|
|
112
|
+
result = my_script.calculate(bars)
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(title_or_metadata, str):
|
|
115
|
+
metadata = IndicatorMetadataConfig(
|
|
116
|
+
title=title_or_metadata,
|
|
117
|
+
short_title=short_title or title_or_metadata,
|
|
118
|
+
overlay=overlay,
|
|
119
|
+
format=format,
|
|
120
|
+
precision=precision,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def decorator(fn: Callable[[BuiltIns], Any]) -> Script:
|
|
124
|
+
return Script(metadata, fn)
|
|
125
|
+
|
|
126
|
+
return decorator
|
|
127
|
+
|
|
128
|
+
# Legacy path: indicator(IndicatorMetadataConfig, setup_fn)
|
|
129
|
+
metadata = title_or_metadata
|
|
130
|
+
assert setup is not None, "setup function required for legacy indicator() API"
|
|
71
131
|
normalized = IndicatorMetadataConfig(
|
|
72
132
|
title=metadata.title,
|
|
73
133
|
short_title=metadata.short_title or metadata.title,
|
|
@@ -10,6 +10,45 @@ from ._types import Bar
|
|
|
10
10
|
SeriesExtractor = Callable[[Bar, int, list[Bar]], float]
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class BuiltIns:
|
|
14
|
+
"""Pre-built Series for standard OHLCV fields and derived composites.
|
|
15
|
+
|
|
16
|
+
Mirrors PineScript's built-in variables: close, open, high, low, volume,
|
|
17
|
+
hl2, hlc3, ohlc4, bar_index, time.
|
|
18
|
+
|
|
19
|
+
Usage::
|
|
20
|
+
|
|
21
|
+
bd = BarData(bars)
|
|
22
|
+
b = BuiltIns(bd)
|
|
23
|
+
sma = ta.sma(b.close, 14)
|
|
24
|
+
change = b.close - b.close[1]
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = (
|
|
28
|
+
"open", "high", "low", "close", "volume",
|
|
29
|
+
"hl2", "hlc3", "ohlc4", "bar_index", "time",
|
|
30
|
+
"_bar_data",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __init__(self, data: list[Bar] | BarData) -> None:
|
|
34
|
+
bd = data if isinstance(data, BarData) else BarData(data)
|
|
35
|
+
self._bar_data = bd
|
|
36
|
+
self.open: Series = Series.from_bars(bd, "open")
|
|
37
|
+
self.high: Series = Series.from_bars(bd, "high")
|
|
38
|
+
self.low: Series = Series.from_bars(bd, "low")
|
|
39
|
+
self.close: Series = Series.from_bars(bd, "close")
|
|
40
|
+
self.volume: Series = Series.from_bars(bd, "volume")
|
|
41
|
+
self.hl2: Series = (self.high + self.low) / 2
|
|
42
|
+
self.hlc3: Series = (self.high + self.low + self.close) / 3
|
|
43
|
+
self.ohlc4: Series = (self.open + self.high + self.low + self.close) / 4
|
|
44
|
+
self.time: Series = Series.from_bars(bd, "time")
|
|
45
|
+
self.bar_index: Series = Series(bd, lambda _b, i, _d: float(i))
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def bar_data(self) -> BarData:
|
|
49
|
+
return self._bar_data
|
|
50
|
+
|
|
51
|
+
|
|
13
52
|
class BarData:
|
|
14
53
|
"""Versioned wrapper around list[Bar] for cache invalidation.
|
|
15
54
|
|
|
@@ -240,6 +279,14 @@ class Series:
|
|
|
240
279
|
|
|
241
280
|
# --- Offset/History ---
|
|
242
281
|
|
|
282
|
+
def __getitem__(self, n: int) -> Series:
|
|
283
|
+
"""Access previous bars: close[1] is equivalent to close.offset(1)."""
|
|
284
|
+
if not isinstance(n, int) or n < 0:
|
|
285
|
+
raise ValueError(f"Series index must be a non-negative integer, got {n!r}")
|
|
286
|
+
if n == 0:
|
|
287
|
+
return self
|
|
288
|
+
return self.offset(n)
|
|
289
|
+
|
|
243
290
|
def offset(self, n: int) -> Series:
|
|
244
291
|
"""Access previous bars (like close[1] in PineScript)."""
|
|
245
292
|
ext = self._extractor
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|