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.
Files changed (98) hide show
  1. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/DEVLOG.md +32 -0
  2. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/PKG-INFO +65 -30
  3. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/README.md +64 -29
  4. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/pyproject.toml +1 -1
  5. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/__init__.py +4 -4
  6. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/_types.py +21 -1
  7. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/_utils.py +42 -0
  8. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/indicator.py +64 -4
  9. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/series.py +47 -0
  10. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/.github/workflows/publish.yml +0 -0
  11. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/.gitignore +0 -0
  12. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/logo.png +0 -0
  13. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/_metadata.py +0 -0
  14. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/adapters/__init__.py +0 -0
  15. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/adapters/simple_input.py +0 -0
  16. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/array.py +0 -0
  17. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/box.py +0 -0
  18. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/chartpoint.py +0 -0
  19. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/color.py +0 -0
  20. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/input_.py +0 -0
  21. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/inputs.py +0 -0
  22. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/label.py +0 -0
  23. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/lib/__init__.py +0 -0
  24. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/lib/zigzag.py +0 -0
  25. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/line.py +0 -0
  26. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/linefill.py +0 -0
  27. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/math_.py +0 -0
  28. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/matrix.py +0 -0
  29. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/plot_.py +0 -0
  30. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/polyline.py +0 -0
  31. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/runtime.py +0 -0
  32. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/runtime_types.py +0 -0
  33. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/str_.py +0 -0
  34. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/ta.py +0 -0
  35. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/ta_series.py +0 -0
  36. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/src/oakscriptpy/time_.py +0 -0
  37. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/__init__.py +0 -0
  38. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/__init__.py +0 -0
  39. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_drawing_objects.py +0 -0
  40. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_final_functions.py +0 -0
  41. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_new_functions.py +0 -0
  42. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_percentile.py +0 -0
  43. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/array/test_search_and_stats.py +0 -0
  44. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/box/__init__.py +0 -0
  45. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/box/test_box.py +0 -0
  46. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/chartpoint/__init__.py +0 -0
  47. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/chartpoint/test_chartpoint.py +0 -0
  48. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/__init__.py +0 -0
  49. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_components.py +0 -0
  50. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_constants.py +0 -0
  51. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_creation.py +0 -0
  52. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/color/test_manipulation.py +0 -0
  53. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/conftest.py +0 -0
  54. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/indicator/__init__.py +0 -0
  55. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/indicator/test_indicator.py +0 -0
  56. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/label/__init__.py +0 -0
  57. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/label/test_label.py +0 -0
  58. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/line/__init__.py +0 -0
  59. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/line/test_line.py +0 -0
  60. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/linefill/__init__.py +0 -0
  61. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/linefill/test_linefill.py +0 -0
  62. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/__init__.py +0 -0
  63. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_algebraic.py +0 -0
  64. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_basic.py +0 -0
  65. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_series_support.py +0 -0
  66. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_sum_series.py +0 -0
  67. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_trigonometric.py +0 -0
  68. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/math/test_utility.py +0 -0
  69. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/__init__.py +0 -0
  70. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/test_foundational.py +0 -0
  71. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/test_linear_algebra.py +0 -0
  72. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/matrix/test_manipulation.py +0 -0
  73. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/polyline/__init__.py +0 -0
  74. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/polyline/test_polyline.py +0 -0
  75. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/__init__.py +0 -0
  76. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_adapters.py +0 -0
  77. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_inputs.py +0 -0
  78. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_runtime.py +0 -0
  79. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/runtime/test_series.py +0 -0
  80. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/__init__.py +0 -0
  81. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_conversion.py +0 -0
  82. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_formatting.py +0 -0
  83. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_manipulation.py +0 -0
  84. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_predicates.py +0 -0
  85. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_search.py +0 -0
  86. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/str/test_whitespace.py +0 -0
  87. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/__init__.py +0 -0
  88. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_ichimoku.py +0 -0
  89. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_functions.py +0 -0
  90. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_functions2.py +0 -0
  91. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_functions3.py +0 -0
  92. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_new_ta_functions.py +0 -0
  93. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_rsi.py +0 -0
  94. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_sma.py +0 -0
  95. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_supertrend.py +0 -0
  96. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_ta_series_integration.py +0 -0
  97. {oakscriptpy-0.1.4 → oakscriptpy-0.2.0}/tests/ta/test_zigzag.py +0 -0
  98. {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.1.4
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 ta, math, color, Series
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
- # Create bars
47
- bars = [
48
- Bar(time=1, open=10.0, high=12.0, low=9.0, close=11.0, volume=100.0),
49
- Bar(time=2, open=11.0, high=13.0, low=10.0, close=12.0, volume=110.0),
50
- Bar(time=3, open=12.0, high=14.0, low=11.0, close=13.0, volume=120.0),
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 = ta.sma(closes, 3)
57
- ema_values = ta.ema(closes, 3)
58
- rsi_values = ta.rsi(closes, 14)
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
- a = Series.from_bars(bars, lambda b: b.close)
96
- b = Series.from_bars(bars, lambda b: b.open)
128
+ b = BuiltIns(bars)
97
129
 
98
130
  # Arithmetic: +, -, *, /, %
99
- spread = a - b
131
+ spread = b.close - b.open
100
132
 
101
133
  # Comparison: >, >=, <, <=, eq(), neq()
102
- bullish = a > b
134
+ bullish = b.close > b.open
103
135
 
104
136
  # Logical: and_(), or_(), not_()
105
- signal = bullish.and_(a > sma_series)
137
+ signal = bullish.and_(b.close > sma)
106
138
 
107
139
  # History access
108
- prev_close = a.offset(1)
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 ta, math, color, Series
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
- # Create bars
26
- bars = [
27
- Bar(time=1, open=10.0, high=12.0, low=9.0, close=11.0, volume=100.0),
28
- Bar(time=2, open=11.0, high=13.0, low=10.0, close=12.0, volume=110.0),
29
- Bar(time=3, open=12.0, high=14.0, low=11.0, close=13.0, volume=120.0),
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 = ta.sma(closes, 3)
36
- ema_values = ta.ema(closes, 3)
37
- rsi_values = ta.rsi(closes, 14)
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
- a = Series.from_bars(bars, lambda b: b.close)
75
- b = Series.from_bars(bars, lambda b: b.open)
107
+ b = BuiltIns(bars)
76
108
 
77
109
  # Arithmetic: +, -, *, /, %
78
- spread = a - b
110
+ spread = b.close - b.open
79
111
 
80
112
  # Comparison: >, >=, <, <=, eq(), neq()
81
- bullish = a > b
113
+ bullish = b.close > b.open
82
114
 
83
115
  # Logical: and_(), or_(), not_()
84
- signal = bullish.and_(a > sma_series)
116
+ signal = bullish.and_(b.close > sma)
85
117
 
86
118
  # History access
87
- prev_close = a.offset(1)
119
+ prev_close = b.close[1]
120
+
121
+ # Conditional (ternary)
122
+ result = bullish.iff(1, -1)
88
123
  ```
89
124
 
90
125
  ## Tests
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oakscriptpy"
3
- version = "0.1.4"
3
+ version = "0.2.0"
4
4
  description = "PineScript-like API for technical analysis in Python"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = []
@@ -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.1.4"
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 + callable checker."""
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
- metadata: IndicatorMetadataConfig,
68
- setup: Callable[[IndicatorContext], None],
69
- ) -> Callable[[], IndicatorInstance]:
70
- """Create an indicator factory function."""
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