polarticks 0.1.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.
- polarticks-0.1.0/PKG-INFO +610 -0
- polarticks-0.1.0/README.md +595 -0
- polarticks-0.1.0/pyproject.toml +48 -0
- polarticks-0.1.0/src/polarticks/__init__.py +135 -0
- polarticks-0.1.0/src/polarticks/_validate.py +18 -0
- polarticks-0.1.0/src/polarticks/levels.py +273 -0
- polarticks-0.1.0/src/polarticks/momentum.py +503 -0
- polarticks-0.1.0/src/polarticks/moving_averages.py +351 -0
- polarticks-0.1.0/src/polarticks/patterns.py +649 -0
- polarticks-0.1.0/src/polarticks/py.typed +0 -0
- polarticks-0.1.0/src/polarticks/trend.py +336 -0
- polarticks-0.1.0/src/polarticks/utils.py +98 -0
- polarticks-0.1.0/src/polarticks/volatility.py +324 -0
- polarticks-0.1.0/src/polarticks/volume.py +251 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: polarticks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Polars-native technical analysis indicator library
|
|
5
|
+
Author: Molingus
|
|
6
|
+
Author-email: Molingus <stephen.c976@gmail.com>
|
|
7
|
+
Requires-Dist: polars>=1.0.0
|
|
8
|
+
Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
|
|
9
|
+
Requires-Dist: pytest-cov>=5.0.0 ; extra == 'dev'
|
|
10
|
+
Requires-Dist: ruff>=0.4.0 ; extra == 'dev'
|
|
11
|
+
Requires-Dist: mypy>=1.10.0 ; extra == 'dev'
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# PolarTicks
|
|
17
|
+
|
|
18
|
+
A Polars-native technical analysis library for Python.
|
|
19
|
+
|
|
20
|
+
Every indicator is implemented directly against the Polars API — no pandas
|
|
21
|
+
conversions, no NumPy loops where a vectorised expression will do. Inputs and
|
|
22
|
+
outputs are plain `pl.Series` or `pl.DataFrame` objects, so the results drop
|
|
23
|
+
straight into your existing Polars pipeline.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Contents
|
|
28
|
+
|
|
29
|
+
- [Installation](#installation)
|
|
30
|
+
- [Quick start](#quick-start)
|
|
31
|
+
- [Input conventions](#input-conventions)
|
|
32
|
+
- [Null-prefix semantics](#null-prefix-semantics)
|
|
33
|
+
- [API reference](#api-reference)
|
|
34
|
+
- [Moving averages](#moving-averages)
|
|
35
|
+
- [Momentum & oscillators](#momentum--oscillators)
|
|
36
|
+
- [Volatility](#volatility)
|
|
37
|
+
- [Trend](#trend)
|
|
38
|
+
- [Volume](#volume)
|
|
39
|
+
- [Levels (pivot points)](#levels-pivot-points)
|
|
40
|
+
- [Candlestick patterns](#candlestick-patterns)
|
|
41
|
+
- [Utilities](#utilities)
|
|
42
|
+
- [Running tests](#running-tests)
|
|
43
|
+
- [Running benchmarks](#running-benchmarks)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# with uv (recommended)
|
|
51
|
+
uv add polarticks
|
|
52
|
+
|
|
53
|
+
# with pip
|
|
54
|
+
pip install polarticks
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python ≥ 3.11 and Polars ≥ 1.0.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import polars as pl
|
|
65
|
+
import polarticks
|
|
66
|
+
|
|
67
|
+
# Load your OHLCV data however you like
|
|
68
|
+
df = pl.read_csv("prices.csv")
|
|
69
|
+
|
|
70
|
+
# Single-series indicators
|
|
71
|
+
close = df["close"]
|
|
72
|
+
rsi_14 = polarticks.rsi(close, period=14) # pl.Series
|
|
73
|
+
ema_20 = polarticks.ema(close, period=20) # pl.Series
|
|
74
|
+
macd_df = polarticks.macd(close) # pl.DataFrame (3 columns)
|
|
75
|
+
|
|
76
|
+
# Multi-column indicators
|
|
77
|
+
bb = polarticks.bollinger_bands(close, period=20) # pl.DataFrame (5 columns)
|
|
78
|
+
atr = polarticks.atr(df, period=14) # pl.Series
|
|
79
|
+
|
|
80
|
+
# Pattern detection
|
|
81
|
+
signals = polarticks.is_bullish_engulfing(df) # pl.Series[bool]
|
|
82
|
+
|
|
83
|
+
# Attach results to your DataFrame
|
|
84
|
+
df = df.with_columns([
|
|
85
|
+
rsi_14.alias("rsi_14"),
|
|
86
|
+
ema_20.alias("ema_20"),
|
|
87
|
+
*bb.get_columns(), # spread all band columns
|
|
88
|
+
signals.alias("bull_engulf"),
|
|
89
|
+
])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### A complete signal pipeline
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import polars as pl
|
|
96
|
+
import polarticks
|
|
97
|
+
|
|
98
|
+
df = pl.read_csv("eurusd_h1.csv")
|
|
99
|
+
close = df["close"]
|
|
100
|
+
|
|
101
|
+
# Compute a fast/slow EMA crossover and RSI filter
|
|
102
|
+
fast = polarticks.ema(close, 9)
|
|
103
|
+
slow = polarticks.ema(close, 21)
|
|
104
|
+
|
|
105
|
+
df = df.with_columns([
|
|
106
|
+
fast.alias("ema_9"),
|
|
107
|
+
slow.alias("ema_21"),
|
|
108
|
+
polarticks.rsi(close, 14).alias("rsi"),
|
|
109
|
+
polarticks.crossover(fast, slow).alias("cross_up"),
|
|
110
|
+
polarticks.crossunder(fast, slow).alias("cross_dn"),
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
# Long entry: crossover AND RSI not overbought
|
|
114
|
+
df = df.with_columns(
|
|
115
|
+
(pl.col("cross_up") & (pl.col("rsi") < 70)).alias("long_entry")
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Input conventions
|
|
122
|
+
|
|
123
|
+
### Single-series indicators
|
|
124
|
+
|
|
125
|
+
Functions like `sma`, `ema`, `rsi`, `roc`, `hma` accept a `pl.Series`:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
result = polarticks.sma(df["close"], period=20)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### OHLC / OHLCV indicators
|
|
132
|
+
|
|
133
|
+
Functions that need multiple price columns accept a `pl.DataFrame`. The
|
|
134
|
+
expected column names are always lowercase: `open`, `high`, `low`, `close`,
|
|
135
|
+
`volume`.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
result = polarticks.atr(df, period=14) # needs high, low, close
|
|
139
|
+
result = polarticks.mfi(df, period=14) # needs high, low, close, volume
|
|
140
|
+
result = polarticks.stochastic(df) # needs high, low, close
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Pivot points
|
|
144
|
+
|
|
145
|
+
Pivot point functions accept individual `pl.Series` (one per OHLC component)
|
|
146
|
+
rather than a DataFrame. This lets you broadcast yesterday's session values
|
|
147
|
+
across today's intraday bars however your data model requires.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Scalar broadcast: yesterday's values repeated across all bars
|
|
151
|
+
n = len(df)
|
|
152
|
+
levels = polarticks.pivot_points_floor(
|
|
153
|
+
prev_high = pl.Series([prev_high] * n),
|
|
154
|
+
prev_low = pl.Series([prev_low] * n),
|
|
155
|
+
prev_close = pl.Series([prev_close] * n),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Rolling: shift the daily OHLC so each bar sees the prior day
|
|
159
|
+
daily = df.group_by_dynamic("date", every="1d").agg(...)
|
|
160
|
+
levels = polarticks.pivot_points_floor(
|
|
161
|
+
prev_high = daily["high"].shift(1),
|
|
162
|
+
prev_low = daily["low"].shift(1),
|
|
163
|
+
prev_close = daily["close"].shift(1),
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Null-prefix semantics
|
|
170
|
+
|
|
171
|
+
Every indicator returns exactly as many leading `null` values as its algorithm
|
|
172
|
+
requires before it can produce a valid output. **No zeros are substituted in
|
|
173
|
+
the warm-up region.** This means:
|
|
174
|
+
|
|
175
|
+
- `sma(n)` → `n − 1` leading nulls
|
|
176
|
+
- `ema(n)` → `n − 1` leading nulls
|
|
177
|
+
- `rsi(n)` → `n` leading nulls (one extra from the initial `diff`)
|
|
178
|
+
- `dema(n)` → `2 × (n − 1)` leading nulls (two EMA passes)
|
|
179
|
+
|
|
180
|
+
When you attach indicator columns to a DataFrame, Polars propagates nulls
|
|
181
|
+
through any downstream arithmetic exactly as you would expect — no silent
|
|
182
|
+
zeroes contaminating your signals.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# Safe: Polars propagates nulls through arithmetic
|
|
186
|
+
signal = pl.col("rsi") < 30 # null where rsi is null, False otherwise (after fill)
|
|
187
|
+
|
|
188
|
+
# If you need to fill before a join or export:
|
|
189
|
+
rsi = polarticks.rsi(close, 14).fill_null(strategy="forward")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## API reference
|
|
195
|
+
|
|
196
|
+
### Moving averages
|
|
197
|
+
|
|
198
|
+
All moving averages accept `(series: pl.Series, period: int)` unless noted
|
|
199
|
+
and return a `pl.Series`.
|
|
200
|
+
|
|
201
|
+
| Function | Description | Leading nulls |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `sma(series, period)` | Simple Moving Average | `period − 1` |
|
|
204
|
+
| `ema(series, period)` | Exponential MA (α = 2/(n+1)) | `period − 1` |
|
|
205
|
+
| `wma(series, period)` | Linearly Weighted MA | `period − 1` |
|
|
206
|
+
| `wilder_smooth(series, period)` | Wilder's RMA (α = 1/n) | `period − 1` |
|
|
207
|
+
| `dema(series, period)` | Double EMA — `2·EMA − EMA(EMA)` | `2·(period−1)` |
|
|
208
|
+
| `tema(series, period)` | Triple EMA | `3·(period−1)` |
|
|
209
|
+
| `hma(series, period)` | Hull MA — reduces lag | `(period−1) + (√period−1)` |
|
|
210
|
+
| `vwma(price, volume, period)` | Volume-Weighted MA | `period − 1` |
|
|
211
|
+
| `mcginley_dynamic(series, period)` | Self-adjusting MA | `period − 1` |
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
close = df["close"]
|
|
215
|
+
volume = df["volume"]
|
|
216
|
+
|
|
217
|
+
sma20 = polarticks.sma(close, 20)
|
|
218
|
+
ema20 = polarticks.ema(close, 20)
|
|
219
|
+
hma20 = polarticks.hma(close, 20)
|
|
220
|
+
vwma20 = polarticks.vwma(close, volume, 20)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**HMA** is particularly useful when you need low lag without excessive noise.
|
|
224
|
+
It runs `WMA(2·WMA(n/2) − WMA(n), √n)` and has a warm-up of
|
|
225
|
+
`(n−1) + (⌈√n⌉−1)` bars.
|
|
226
|
+
|
|
227
|
+
**McGinley Dynamic** self-corrects its smoothing speed based on how fast price
|
|
228
|
+
is moving relative to the current indicator value. Seed is the SMA of the
|
|
229
|
+
first `period` bars.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Momentum & oscillators
|
|
234
|
+
|
|
235
|
+
#### `rsi(series, period=14)` → `pl.Series`
|
|
236
|
+
|
|
237
|
+
Relative Strength Index via Wilder's smoothing. Values in [0, 100].
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
rsi = polarticks.rsi(df["close"], 14)
|
|
241
|
+
overbought = rsi > 70
|
|
242
|
+
oversold = rsi < 30
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### `macd(series, fast=12, slow=26, signal=9)` → `pl.DataFrame`
|
|
246
|
+
|
|
247
|
+
Returns a DataFrame with three columns: `macd_line`, `macd_signal`,
|
|
248
|
+
`macd_histogram`.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
m = polarticks.macd(df["close"])
|
|
252
|
+
# m["macd_line"] — fast EMA minus slow EMA
|
|
253
|
+
# m["macd_signal"] — EMA of the MACD line
|
|
254
|
+
# m["macd_histogram"] — line minus signal
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### `stochastic(ohlc, k_period=14, d_period=3)` → `pl.DataFrame`
|
|
258
|
+
|
|
259
|
+
Returns `stoch_k` and `stoch_d`. Both range from 0 to 100.
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
st = polarticks.stochastic(df)
|
|
263
|
+
cross_up = polarticks.crossover(st["stoch_k"], st["stoch_d"])
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### `williams_r(ohlc, period=14)` → `pl.Series`
|
|
267
|
+
|
|
268
|
+
Williams %R in the range [−100, 0]. Readings near 0 are overbought; near
|
|
269
|
+
−100 are oversold.
|
|
270
|
+
|
|
271
|
+
#### `cci(ohlc, period=20)` → `pl.Series`
|
|
272
|
+
|
|
273
|
+
Commodity Channel Index. Readings above +100 suggest overbought; below −100
|
|
274
|
+
suggest oversold.
|
|
275
|
+
|
|
276
|
+
#### `roc(series, period=10)` → `pl.Series`
|
|
277
|
+
|
|
278
|
+
Rate of Change as a percentage: `100 × (close − close[n]) / close[n]`.
|
|
279
|
+
Has `period` leading nulls (one more than most indicators) because it uses
|
|
280
|
+
`shift(period)`.
|
|
281
|
+
|
|
282
|
+
#### `mfi(ohlcv, period=14)` → `pl.Series`
|
|
283
|
+
|
|
284
|
+
Money Flow Index — the volume-weighted version of RSI. Requires a `volume`
|
|
285
|
+
column. Values in [0, 100].
|
|
286
|
+
|
|
287
|
+
#### `cmf(ohlcv, period=20)` → `pl.Series`
|
|
288
|
+
|
|
289
|
+
Chaikin Money Flow. Measures buying vs. selling pressure in the range [−1, 1].
|
|
290
|
+
Positive values indicate accumulation.
|
|
291
|
+
|
|
292
|
+
#### `tsi(series, slow=25, fast=13)` → `pl.Series`
|
|
293
|
+
|
|
294
|
+
True Strength Index — double-smoothed momentum oscillator. Values in
|
|
295
|
+
(−100, +100). Signal line: apply `ema(tsi, 7)` to the output.
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
tsi_vals = polarticks.tsi(df["close"])
|
|
299
|
+
signal = polarticks.ema(tsi_vals.fill_null(0.0), 7) # fill before second EMA
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### `ultimate_oscillator(ohlc, period1=7, period2=14, period3=28)` → `pl.Series`
|
|
303
|
+
|
|
304
|
+
Weighted blend of three time-frame buying-pressure ratios. Values in [0, 100].
|
|
305
|
+
Warm-up is `period3 − 1` bars.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
### Volatility
|
|
310
|
+
|
|
311
|
+
#### `true_range(ohlc)` → `pl.Series`
|
|
312
|
+
|
|
313
|
+
Single-bar True Range — the greatest of `H−L`, `|H−prev_C|`, `|L−prev_C|`.
|
|
314
|
+
The first bar has no prior close; its TR collapses to `H−L` (Wilder's
|
|
315
|
+
convention). Zero leading nulls.
|
|
316
|
+
|
|
317
|
+
#### `atr(ohlc, period=14)` → `pl.Series`
|
|
318
|
+
|
|
319
|
+
Average True Range via Wilder's smoothing. `period − 1` leading nulls.
|
|
320
|
+
|
|
321
|
+
#### `bollinger_bands(series, period=20, num_std=2.0)` → `pl.DataFrame`
|
|
322
|
+
|
|
323
|
+
Returns five columns:
|
|
324
|
+
|
|
325
|
+
| Column | Description |
|
|
326
|
+
|---|---|
|
|
327
|
+
| `bb_middle_{period}` | SMA of close |
|
|
328
|
+
| `bb_upper_{period}` | Middle + `num_std` × rolling std |
|
|
329
|
+
| `bb_lower_{period}` | Middle − `num_std` × rolling std |
|
|
330
|
+
| `bb_pct_b_{period}` | Position within the band (0 = lower, 1 = upper) |
|
|
331
|
+
| `bb_width_{period}` | (Upper − Lower) / Middle |
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
bb = polarticks.bollinger_bands(df["close"], 20)
|
|
335
|
+
squeeze = bb["bb_width_20"] < bb["bb_width_20"].rolling_mean(20)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### `keltner_channels(ohlc, ema_period=20, atr_period=10, multiplier=2.0)` → `pl.DataFrame`
|
|
339
|
+
|
|
340
|
+
Returns `kc_middle`, `kc_upper`, `kc_lower`. Uses ATR for band width rather
|
|
341
|
+
than standard deviation, making the channels less reactive to individual large
|
|
342
|
+
moves.
|
|
343
|
+
|
|
344
|
+
#### `chaikin_volatility(ohlc, ema_period=10, roc_period=10)` → `pl.Series`
|
|
345
|
+
|
|
346
|
+
Rate of change of the EMA of the high-low range. Rising values signal
|
|
347
|
+
increasing volatility. Leading nulls: `(ema_period − 1) + roc_period`.
|
|
348
|
+
|
|
349
|
+
#### `historical_volatility(series, period=20, annualise=True, trading_days=252)` → `pl.Series`
|
|
350
|
+
|
|
351
|
+
Rolling annualised standard deviation of log returns. Set `trading_days=365`
|
|
352
|
+
for crypto or `260` for FX. `period` leading nulls.
|
|
353
|
+
|
|
354
|
+
#### `ulcer_index(series, period=14)` → `pl.Series`
|
|
355
|
+
|
|
356
|
+
Drawdown-based volatility: `√(mean(pct_drawdown², period))`. Only penalises
|
|
357
|
+
downside moves; useful for risk-adjusted metrics like the Ulcer Performance
|
|
358
|
+
Index. Leading nulls: `2 × (period − 1)` (rolling max then rolling mean).
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### Trend
|
|
363
|
+
|
|
364
|
+
#### `donchian_channels(ohlc, period=20)` → `pl.DataFrame`
|
|
365
|
+
|
|
366
|
+
Returns `dc_upper_{period}`, `dc_lower_{period}`, `dc_middle_{period}`.
|
|
367
|
+
Breakout above the upper channel or below the lower channel signals a
|
|
368
|
+
trend initiation.
|
|
369
|
+
|
|
370
|
+
#### `adx(ohlc, period=14)` → `pl.DataFrame`
|
|
371
|
+
|
|
372
|
+
Average Directional Index with directional components.
|
|
373
|
+
|
|
374
|
+
| Column | Description |
|
|
375
|
+
|---|---|
|
|
376
|
+
| `adx_{period}` | Trend strength (0–100; >25 = trending) |
|
|
377
|
+
| `plus_di_{period}` | Bullish directional movement |
|
|
378
|
+
| `minus_di_{period}` | Bearish directional movement |
|
|
379
|
+
|
|
380
|
+
Leading nulls: `period − 1` for the DI columns; `2 × (period − 1)` for ADX
|
|
381
|
+
(it is Wilder-smoothed DX, which is itself Wilder-smoothed).
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
result = polarticks.adx(df, 14)
|
|
385
|
+
trending = result["adx_14"] > 25
|
|
386
|
+
bull_trend = result["plus_di_14"] > result["minus_di_14"]
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### `supertrend(ohlc, period=7, multiplier=3.0)` → `pl.DataFrame`
|
|
390
|
+
|
|
391
|
+
ATR-based trailing stop that also indicates trend direction.
|
|
392
|
+
|
|
393
|
+
| Column | Description |
|
|
394
|
+
|---|---|
|
|
395
|
+
| `supertrend` | Band level (support in uptrend, resistance in downtrend) |
|
|
396
|
+
| `supertrend_direction` | `+1` (bullish) or `−1` (bearish) |
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
st = polarticks.supertrend(df, period=10, multiplier=2.0)
|
|
400
|
+
entries = st["supertrend_direction"].diff() == 2 # flipped to bullish
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### `parabolic_sar(ohlc, initial_af=0.02, step_af=0.02, max_af=0.20)` → `pl.DataFrame`
|
|
404
|
+
|
|
405
|
+
Parabolic SAR dot plot. Returns `psar` (price level) and `psar_direction`
|
|
406
|
+
(`+1` uptrend / `−1` downtrend). One leading null (initialised from bar 1).
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
sar = polarticks.parabolic_sar(df)
|
|
410
|
+
flip_to_bull = sar["psar_direction"].diff() == 2
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### Volume
|
|
416
|
+
|
|
417
|
+
#### `obv(ohlcv)` → `pl.Series`
|
|
418
|
+
|
|
419
|
+
On-Balance Volume — running cumulative sum of signed volume. Volume is added
|
|
420
|
+
on up-bars and subtracted on down-bars. No leading nulls; starts accumulating
|
|
421
|
+
from bar 0.
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
obv = polarticks.obv(df)
|
|
425
|
+
obv_trend = polarticks.ema(obv, 20) # smooth OBV to spot divergences
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### `vwap(ohlcv, session_start_hour=22)` → `pl.Series`
|
|
429
|
+
|
|
430
|
+
Session-anchored VWAP. If your DataFrame has a `time` column (Polars
|
|
431
|
+
`Datetime`), a new session is started at every bar whose UTC hour equals
|
|
432
|
+
`session_start_hour`. Without a `time` column the entire series is treated
|
|
433
|
+
as one session. No leading nulls.
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
vwap = polarticks.vwap(df, session_start_hour=0) # midnight UTC sessions
|
|
437
|
+
above_vwap = df["close"] > vwap
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
#### `vwap_bands(ohlcv, session_start_hour=22)` → `pl.DataFrame`
|
|
441
|
+
|
|
442
|
+
VWAP with ±1σ and ±2σ volume-weighted standard-deviation bands. Returns
|
|
443
|
+
`vwap`, `upper_1`, `lower_1`, `upper_2`, `lower_2`. No leading nulls.
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
### Levels (pivot points)
|
|
448
|
+
|
|
449
|
+
All pivot point functions accept `pl.Series` arguments (one per price
|
|
450
|
+
component) and return a `pl.DataFrame`. Pass yesterday's values — scalar
|
|
451
|
+
broadcast or a shifted daily series — aligned to your current-session bars.
|
|
452
|
+
|
|
453
|
+
#### `pivot_points_floor(prev_high, prev_low, prev_close)` → `pl.DataFrame`
|
|
454
|
+
|
|
455
|
+
Classic floor-trader pivots. Columns: `pp`, `r1`, `r2`, `r3`, `s1`, `s2`, `s3`.
|
|
456
|
+
|
|
457
|
+
#### `pivot_points_camarilla(prev_high, prev_low, prev_close)` → `pl.DataFrame`
|
|
458
|
+
|
|
459
|
+
Camarilla equation (multiplier 1.1). Produces tighter intraday levels suited
|
|
460
|
+
to mean-reversion scalping. Columns: `cam_r1`–`cam_r4`, `cam_s1`–`cam_s4`.
|
|
461
|
+
|
|
462
|
+
#### `pivot_points_fibonacci(prev_high, prev_low, prev_close)` → `pl.DataFrame`
|
|
463
|
+
|
|
464
|
+
Fibonacci-ratio levels (0.382, 0.618, 1.000 × range).
|
|
465
|
+
Columns: `fib_pp`, `fib_r1`–`fib_r3`, `fib_s1`–`fib_s3`.
|
|
466
|
+
|
|
467
|
+
#### `pivot_points_woodie(prev_high, prev_low, prev_close)` → `pl.DataFrame`
|
|
468
|
+
|
|
469
|
+
Double-weights the prior close. Columns: `wood_pp`, `wood_r1`, `wood_r2`,
|
|
470
|
+
`wood_s1`, `wood_s2`.
|
|
471
|
+
|
|
472
|
+
#### `pivot_points_demark(prev_open, prev_high, prev_low, prev_close)` → `pl.DataFrame`
|
|
473
|
+
|
|
474
|
+
Adapts the formula based on whether the prior session closed above, below, or
|
|
475
|
+
equal to its open. Returns a single resistance and support level.
|
|
476
|
+
Columns: `dm_pp`, `dm_r1`, `dm_s1`.
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
n = len(df)
|
|
480
|
+
levels = polarticks.pivot_points_floor(
|
|
481
|
+
pl.Series([yesterday_high] * n),
|
|
482
|
+
pl.Series([yesterday_low] * n),
|
|
483
|
+
pl.Series([yesterday_close] * n),
|
|
484
|
+
)
|
|
485
|
+
df = df.with_columns(levels.get_columns())
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
### Candlestick patterns
|
|
491
|
+
|
|
492
|
+
All pattern functions accept a `pl.DataFrame` with `open`, `high`, `low`,
|
|
493
|
+
`close` columns and return a Boolean `pl.Series` — `True` on bars where the
|
|
494
|
+
pattern is present, `False` everywhere else (including leading bars that
|
|
495
|
+
cannot satisfy the look-back requirement).
|
|
496
|
+
|
|
497
|
+
#### Single-bar patterns
|
|
498
|
+
|
|
499
|
+
| Function | Description |
|
|
500
|
+
|---|---|
|
|
501
|
+
| `is_doji(ohlc, threshold=0.1)` | Body is < 10% of the bar's range |
|
|
502
|
+
| `is_pin_bar_bullish(ohlc, wick_ratio=0.6, body_ratio=0.25)` | Hammer: small body, long lower wick |
|
|
503
|
+
| `is_pin_bar_bearish(ohlc, wick_ratio=0.6, body_ratio=0.25)` | Shooting star: small body, long upper wick |
|
|
504
|
+
|
|
505
|
+
#### Two-bar patterns
|
|
506
|
+
|
|
507
|
+
| Function | Description |
|
|
508
|
+
|---|---|
|
|
509
|
+
| `is_bullish_engulfing(ohlc)` | Bearish bar followed by a larger bullish bar that engulfs it |
|
|
510
|
+
| `is_bearish_engulfing(ohlc)` | Bullish bar followed by a larger bearish bar that engulfs it |
|
|
511
|
+
| `is_inside_bar(ohlc)` | Current bar's range is entirely within the prior bar's range |
|
|
512
|
+
| `is_bullish_harami(ohlc)` | Small bullish body inside a large prior bearish body |
|
|
513
|
+
| `is_bearish_harami(ohlc)` | Small bearish body inside a large prior bullish body |
|
|
514
|
+
|
|
515
|
+
#### Three-bar patterns
|
|
516
|
+
|
|
517
|
+
| Function | Description |
|
|
518
|
+
|---|---|
|
|
519
|
+
| `is_three_white_soldiers(ohlc, body_ratio=0.5)` | Three consecutive advancing bullish candles |
|
|
520
|
+
| `is_three_black_crows(ohlc, body_ratio=0.5)` | Three consecutive declining bearish candles |
|
|
521
|
+
| `is_morning_star(ohlc, body_ratio=0.3, star_body_ratio=0.15)` | Bearish → small star → bullish reversal |
|
|
522
|
+
| `is_evening_star(ohlc, body_ratio=0.3, star_body_ratio=0.15)` | Bullish → small star → bearish reversal |
|
|
523
|
+
|
|
524
|
+
```python
|
|
525
|
+
# Combine patterns and indicators for a signal
|
|
526
|
+
bull_signals = (
|
|
527
|
+
polarticks.is_bullish_engulfing(df)
|
|
528
|
+
| polarticks.is_morning_star(df)
|
|
529
|
+
| polarticks.is_pin_bar_bullish(df)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
df = df.with_columns([
|
|
533
|
+
bull_signals.alias("bull_pattern"),
|
|
534
|
+
polarticks.rsi(df["close"], 14).alias("rsi"),
|
|
535
|
+
])
|
|
536
|
+
|
|
537
|
+
entries = df.filter(pl.col("bull_pattern") & (pl.col("rsi") < 40))
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
### Utilities
|
|
543
|
+
|
|
544
|
+
#### `crossover(fast, slow, atol=0.0)` → `pl.Series[bool]`
|
|
545
|
+
|
|
546
|
+
`True` on the single bar where `fast` crosses above `slow`. The optional
|
|
547
|
+
`atol` prevents double-signals from floating-point noise right at the crossing
|
|
548
|
+
price.
|
|
549
|
+
|
|
550
|
+
#### `crossunder(fast, slow, atol=0.0)` → `pl.Series[bool]`
|
|
551
|
+
|
|
552
|
+
`True` on the single bar where `fast` crosses below `slow`.
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
fast = polarticks.ema(close, 9)
|
|
556
|
+
slow = polarticks.ema(close, 21)
|
|
557
|
+
|
|
558
|
+
long_entry = polarticks.crossover(fast, slow)
|
|
559
|
+
short_entry = polarticks.crossunder(fast, slow)
|
|
560
|
+
|
|
561
|
+
# Noise-tolerant version for choppy markets
|
|
562
|
+
long_entry = polarticks.crossover(fast, slow, atol=0.05)
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
#### `log_returns(series)` → `pl.Series`
|
|
566
|
+
|
|
567
|
+
Bar-to-bar log returns: `ln(price[t] / price[t-1])`. One leading null.
|
|
568
|
+
|
|
569
|
+
#### `simple_returns(series)` → `pl.Series`
|
|
570
|
+
|
|
571
|
+
Bar-to-bar simple (arithmetic) returns. One leading null.
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Running tests
|
|
576
|
+
|
|
577
|
+
```bash
|
|
578
|
+
uv run pytest tests/unit/ # 232 unit tests
|
|
579
|
+
uv run pytest tests/ # all tests (includes benchmarks — takes ~90 s)
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
To run only the null-prefix consistency audit:
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
uv run pytest tests/unit/test_null_prefix.py -v
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Type-check the package:
|
|
589
|
+
|
|
590
|
+
```bash
|
|
591
|
+
uv run mypy src/polarticks/ --strict
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Running benchmarks
|
|
597
|
+
|
|
598
|
+
The benchmark suite exercises every indicator on a 100 000-bar OHLCV series:
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
uv run pytest tests/benchmark/ --benchmark-only
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Save a baseline and compare across changes:
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
uv run pytest tests/benchmark/ --benchmark-only --benchmark-save=baseline
|
|
608
|
+
# ... make changes ...
|
|
609
|
+
uv run pytest tests/benchmark/ --benchmark-only --benchmark-compare=baseline
|
|
610
|
+
```
|