py-alpha-lib 0.2.2__tar.gz → 0.2.4__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.
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/CHANGELOG.md +17 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/Cargo.lock +1 -1
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/Cargo.toml +2 -1
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/PKG-INFO +5 -1
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/README.md +4 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo/algo_gen.py +93 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo.md +4 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/context.py +43 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/to_python.py +1 -1
- py_alpha_lib-0.2.4/src/algo/alpha.rs +225 -0
- py_alpha_lib-0.2.4/src/algo/beta.rs +261 -0
- py_alpha_lib-0.2.4/src/algo/drawdown.rs +225 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/mod.rs +8 -0
- py_alpha_lib-0.2.4/src/algo/sharpe.rs +245 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.agent/skills/add_algo/SKILL.md +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.github/workflows/CI.yml +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.gitignore +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.nwa-config.yaml +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/LICENSE +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/articles/001.md +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/articles/COMPARISON.md +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/benchmarks/benchmark_alpha101.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/benchmarks/benchmark_pandas.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/benchmarks/benchmark_polars_ta.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/build.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/al/__init__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/al/alpha191.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/al/alpha191_context.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/alpha191.txt +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/main.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/al/__init__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/al/alpha101.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/al/alpha101_context.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/alpha101.txt +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/main.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pd_/__init__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pd_/alpha101_adjusted.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pl_/__init__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pl_/alpha101_adjusted.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/result.md +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/pyproject.toml +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/__init__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo/__init__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo/algo.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/__init__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/__main__.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/alpha.lark +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/parser.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/conftest.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_grammar.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_rank.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_talib.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_to_python.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/rustfmt.toml +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/backfill.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/context.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/cross.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/ema.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/entropy.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/error.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/extremum.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/group.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/ma.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/misc.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/moments.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/neutralize.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/quantile.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/rank.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/returns.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/scan.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/series.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/skip_nan_window.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/slope.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/stats.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/stddev.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/sum.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/zscore.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/lib.rs +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/tests/rank.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/tests/usage.py +0 -0
- {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/tests/verify_sumif.py +0 -0
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# ChangeLog
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
## [0.2.4] - 2026-04-27
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- ALPHA: calculate alpha for a strategy
|
|
9
|
+
- BETA: calculate beta for a strategy
|
|
10
|
+
- SHARPE: calculate sharpe ratio for a strategy
|
|
11
|
+
- MAX_DRAWDOWN: calculate max drawdown of a series
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.2.3] - 2026-04-13
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- ExecContext: TS_QUANTILE, SLOPE, INTERCEPT, DMA, SUMBARS, BARSLAST, BARSSINCE, BINS, FRET, RCROSS, RLONGCROSS
|
|
19
|
+
|
|
3
20
|
## [0.2.2] - 2026-04-11
|
|
4
21
|
|
|
5
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "alpha"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
authors = ["ElseJJ"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -9,6 +9,7 @@ readme = "README.md"
|
|
|
9
9
|
[lib]
|
|
10
10
|
name = "alpha"
|
|
11
11
|
crate-type = ["cdylib", "rlib"]
|
|
12
|
+
doctest = false
|
|
12
13
|
|
|
13
14
|
[dependencies]
|
|
14
15
|
pyo3 = { version = "0.28", features = ["abi3"] }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-alpha-lib
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -172,9 +172,11 @@ Naming Rules:
|
|
|
172
172
|
|
|
173
173
|
| Name | Description |
|
|
174
174
|
|---|---|
|
|
175
|
+
| ALPHA | Rolling Jensen's Alpha of asset returns against benchmark returns. |
|
|
175
176
|
| BACKFILL | Forward-fill NaN values with the last valid observation |
|
|
176
177
|
| BARSLAST | Calculate number of bars since last condition true |
|
|
177
178
|
| BARSSINCE | Calculate number of bars since first condition true |
|
|
179
|
+
| BETA | Rolling Beta coefficient of asset returns against benchmark returns. |
|
|
178
180
|
| BINS | Discretize the input into n bins, the ctx.groups() is the number of groups |
|
|
179
181
|
| CC_RANK | Calculate rank percentage cross group dimension, the ctx.groups() is the number of groups Same value are averaged |
|
|
180
182
|
| CC_ZSCORE | Calculate cross-sectional Z-Score across groups at each time step |
|
|
@@ -199,6 +201,7 @@ Naming Rules:
|
|
|
199
201
|
| LONGCROSS | For 2 arrays A and B, return true if previous N periods A < B, Current A >= B |
|
|
200
202
|
| LWMA | Linear Weighted Moving Average |
|
|
201
203
|
| MA | Simple Moving Average, also known as arithmetic moving average |
|
|
204
|
+
| MAX_DRAWDOWN | Rolling Maximum Drawdown. |
|
|
202
205
|
| MIN_MAX_DIFF | Calculate rolling min-max difference (range) over a moving window |
|
|
203
206
|
| MOMENT | Calculate rolling k-th central moment over a moving window |
|
|
204
207
|
| NEUTRALIZE | Neutralize the effect of a categorical variable on a numeric variable |
|
|
@@ -212,6 +215,7 @@ Naming Rules:
|
|
|
212
215
|
| RLONGCROSS | For 2 arrays A and B, return true if previous N periods A > B, Current A <= B |
|
|
213
216
|
| SCAN_ADD | Conditional cumulative add: r[t] = r[t-1] + (cond[t] ? input[t] : 0) |
|
|
214
217
|
| SCAN_MUL | Conditional cumulative multiply: r[t] = r[t-1] * (cond[t] ? input[t] : 1) |
|
|
218
|
+
| SHARPE | Rolling Sharpe Ratio of returns. |
|
|
215
219
|
| SKEWNESS | Calculate rolling sample Skewness over a moving window |
|
|
216
220
|
| SLOPE | Linear Regression Slope |
|
|
217
221
|
| SMA | Exponential Moving Average (variant of well-known EMA) weight = m / n |
|
|
@@ -157,9 +157,11 @@ Naming Rules:
|
|
|
157
157
|
|
|
158
158
|
| Name | Description |
|
|
159
159
|
|---|---|
|
|
160
|
+
| ALPHA | Rolling Jensen's Alpha of asset returns against benchmark returns. |
|
|
160
161
|
| BACKFILL | Forward-fill NaN values with the last valid observation |
|
|
161
162
|
| BARSLAST | Calculate number of bars since last condition true |
|
|
162
163
|
| BARSSINCE | Calculate number of bars since first condition true |
|
|
164
|
+
| BETA | Rolling Beta coefficient of asset returns against benchmark returns. |
|
|
163
165
|
| BINS | Discretize the input into n bins, the ctx.groups() is the number of groups |
|
|
164
166
|
| CC_RANK | Calculate rank percentage cross group dimension, the ctx.groups() is the number of groups Same value are averaged |
|
|
165
167
|
| CC_ZSCORE | Calculate cross-sectional Z-Score across groups at each time step |
|
|
@@ -184,6 +186,7 @@ Naming Rules:
|
|
|
184
186
|
| LONGCROSS | For 2 arrays A and B, return true if previous N periods A < B, Current A >= B |
|
|
185
187
|
| LWMA | Linear Weighted Moving Average |
|
|
186
188
|
| MA | Simple Moving Average, also known as arithmetic moving average |
|
|
189
|
+
| MAX_DRAWDOWN | Rolling Maximum Drawdown. |
|
|
187
190
|
| MIN_MAX_DIFF | Calculate rolling min-max difference (range) over a moving window |
|
|
188
191
|
| MOMENT | Calculate rolling k-th central moment over a moving window |
|
|
189
192
|
| NEUTRALIZE | Neutralize the effect of a categorical variable on a numeric variable |
|
|
@@ -197,6 +200,7 @@ Naming Rules:
|
|
|
197
200
|
| RLONGCROSS | For 2 arrays A and B, return true if previous N periods A > B, Current A <= B |
|
|
198
201
|
| SCAN_ADD | Conditional cumulative add: r[t] = r[t-1] + (cond[t] ? input[t] : 0) |
|
|
199
202
|
| SCAN_MUL | Conditional cumulative multiply: r[t] = r[t-1] * (cond[t] ? input[t] : 1) |
|
|
203
|
+
| SHARPE | Rolling Sharpe Ratio of returns. |
|
|
200
204
|
| SKEWNESS | Calculate rolling sample Skewness over a moving window |
|
|
201
205
|
| SLOPE | Linear Regression Slope |
|
|
202
206
|
| SMA | Exponential Moving Average (variant of well-known EMA) weight = m / n |
|
|
@@ -18,6 +18,30 @@ def _to_bool(a):
|
|
|
18
18
|
return a
|
|
19
19
|
return a.astype(bool)
|
|
20
20
|
|
|
21
|
+
def ALPHA(
|
|
22
|
+
input: np.ndarray | list[np.ndarray], benchmark: np.ndarray | list[np.ndarray], periods: int
|
|
23
|
+
) -> np.ndarray | list[np.ndarray]:
|
|
24
|
+
"""
|
|
25
|
+
Rolling Jensen's Alpha of asset returns against benchmark returns.
|
|
26
|
+
|
|
27
|
+
Alpha = mean(input) - Beta * mean(benchmark)
|
|
28
|
+
Measures excess return of an asset relative to its expected return given beta.
|
|
29
|
+
|
|
30
|
+
Ref: https://en.wikipedia.org/wiki/Jensen%27s_alpha
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(input, list) and isinstance(benchmark, list):
|
|
33
|
+
input = [_to_f64(x) for x in input]
|
|
34
|
+
benchmark = [_to_f64(x) for x in benchmark]
|
|
35
|
+
r = [np.empty_like(x) for x in input]
|
|
36
|
+
_algo.alpha(r, input, benchmark, periods)
|
|
37
|
+
return r
|
|
38
|
+
else:
|
|
39
|
+
input = _to_f64(input)
|
|
40
|
+
benchmark = _to_f64(benchmark)
|
|
41
|
+
r = np.empty_like(input)
|
|
42
|
+
_algo.alpha(r, input, benchmark, periods)
|
|
43
|
+
return r
|
|
44
|
+
|
|
21
45
|
def BACKFILL(
|
|
22
46
|
input: np.ndarray | list[np.ndarray]
|
|
23
47
|
) -> np.ndarray | list[np.ndarray]:
|
|
@@ -76,6 +100,30 @@ def BARSSINCE(
|
|
|
76
100
|
_algo.barssince(r, input)
|
|
77
101
|
return r
|
|
78
102
|
|
|
103
|
+
def BETA(
|
|
104
|
+
input: np.ndarray | list[np.ndarray], benchmark: np.ndarray | list[np.ndarray], periods: int
|
|
105
|
+
) -> np.ndarray | list[np.ndarray]:
|
|
106
|
+
"""
|
|
107
|
+
Rolling Beta coefficient of asset returns against benchmark returns.
|
|
108
|
+
|
|
109
|
+
Beta = Covariance(input, benchmark) / Variance(benchmark)
|
|
110
|
+
Measures systematic risk of an asset relative to the market.
|
|
111
|
+
|
|
112
|
+
Ref: https://en.wikipedia.org/wiki/Beta_(finance)
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(input, list) and isinstance(benchmark, list):
|
|
115
|
+
input = [_to_f64(x) for x in input]
|
|
116
|
+
benchmark = [_to_f64(x) for x in benchmark]
|
|
117
|
+
r = [np.empty_like(x) for x in input]
|
|
118
|
+
_algo.beta(r, input, benchmark, periods)
|
|
119
|
+
return r
|
|
120
|
+
else:
|
|
121
|
+
input = _to_f64(input)
|
|
122
|
+
benchmark = _to_f64(benchmark)
|
|
123
|
+
r = np.empty_like(input)
|
|
124
|
+
_algo.beta(r, input, benchmark, periods)
|
|
125
|
+
return r
|
|
126
|
+
|
|
79
127
|
def BINS(
|
|
80
128
|
input: np.ndarray | list[np.ndarray], bins: int
|
|
81
129
|
) -> np.ndarray | list[np.ndarray]:
|
|
@@ -542,6 +590,29 @@ def MA(
|
|
|
542
590
|
_algo.ma(r, input, periods)
|
|
543
591
|
return r
|
|
544
592
|
|
|
593
|
+
def MAX_DRAWDOWN(
|
|
594
|
+
input: np.ndarray | list[np.ndarray], periods: int
|
|
595
|
+
) -> np.ndarray | list[np.ndarray]:
|
|
596
|
+
"""
|
|
597
|
+
Rolling Maximum Drawdown.
|
|
598
|
+
|
|
599
|
+
MaxDrawdown = minimum peak-to-trough decline within the rolling window.
|
|
600
|
+
Result is expressed as a negative return (e.g. -0.2 means 20% drawdown from peak).
|
|
601
|
+
Input should be a price or equity curve series.
|
|
602
|
+
|
|
603
|
+
Ref: https://en.wikipedia.org/wiki/Drawdown_(economics)
|
|
604
|
+
"""
|
|
605
|
+
if isinstance(input, list):
|
|
606
|
+
input = [_to_f64(x) for x in input]
|
|
607
|
+
r = [np.empty_like(x) for x in input]
|
|
608
|
+
_algo.max_drawdown(r, input, periods)
|
|
609
|
+
return r
|
|
610
|
+
else:
|
|
611
|
+
input = _to_f64(input)
|
|
612
|
+
r = np.empty_like(input)
|
|
613
|
+
_algo.max_drawdown(r, input, periods)
|
|
614
|
+
return r
|
|
615
|
+
|
|
545
616
|
def MIN_MAX_DIFF(
|
|
546
617
|
input: np.ndarray | list[np.ndarray], periods: int
|
|
547
618
|
) -> np.ndarray | list[np.ndarray]:
|
|
@@ -811,6 +882,28 @@ def SCAN_MUL(
|
|
|
811
882
|
_algo.scan_mul(r, input, condition)
|
|
812
883
|
return r
|
|
813
884
|
|
|
885
|
+
def SHARPE(
|
|
886
|
+
input: np.ndarray | list[np.ndarray], periods: int
|
|
887
|
+
) -> np.ndarray | list[np.ndarray]:
|
|
888
|
+
"""
|
|
889
|
+
Rolling Sharpe Ratio of returns.
|
|
890
|
+
|
|
891
|
+
Sharpe = mean(returns) / stddev(returns)
|
|
892
|
+
Measures risk-adjusted return over a rolling window.
|
|
893
|
+
|
|
894
|
+
Ref: https://en.wikipedia.org/wiki/Sharpe_ratio
|
|
895
|
+
"""
|
|
896
|
+
if isinstance(input, list):
|
|
897
|
+
input = [_to_f64(x) for x in input]
|
|
898
|
+
r = [np.empty_like(x) for x in input]
|
|
899
|
+
_algo.sharpe(r, input, periods)
|
|
900
|
+
return r
|
|
901
|
+
else:
|
|
902
|
+
input = _to_f64(input)
|
|
903
|
+
r = np.empty_like(input)
|
|
904
|
+
_algo.sharpe(r, input, periods)
|
|
905
|
+
return r
|
|
906
|
+
|
|
814
907
|
def SKEWNESS(
|
|
815
908
|
input: np.ndarray | list[np.ndarray], periods: int
|
|
816
909
|
) -> np.ndarray | list[np.ndarray]:
|
|
@@ -2,9 +2,11 @@ List of available functions with python type hints:
|
|
|
2
2
|
|
|
3
3
|
the `np.ndarray` is `ndarray` type in `numpy` package
|
|
4
4
|
|
|
5
|
+
- ALPHA(input: np.ndarray[float], benchmark: np.ndarray[float], periods: int): Rolling Jensen's Alpha of asset returns against benchmark returns. Alpha = mean(input) - Beta * mean(benchmark) Measures excess return of an asset relative to its expected return given beta.
|
|
5
6
|
- BACKFILL(input: np.ndarray[float]): Forward-fill NaN values with the last valid observation Iterates forward through each group; if x[i] is NaN, copies the last valid value. Leading NaNs (before any valid value) remain NaN.
|
|
6
7
|
- BARSLAST(input: np.ndarray[bool]): Calculate number of bars since last condition true
|
|
7
8
|
- BARSSINCE(input: np.ndarray[bool]): Calculate number of bars since first condition true
|
|
9
|
+
- BETA(input: np.ndarray[float], benchmark: np.ndarray[float], periods: int): Rolling Beta coefficient of asset returns against benchmark returns. Beta = Covariance(input, benchmark) / Variance(benchmark) Measures systematic risk of an asset relative to the market.
|
|
8
10
|
- BINS(input: np.ndarray[float], bins: int): Discretize the input into n bins, the ctx.groups() is the number of groups Bins are 0-based index. Same value are assigned to the same bin.
|
|
9
11
|
- CC_RANK(input: np.ndarray[float]): Calculate rank percentage cross group dimension, the ctx.groups() is the number of groups Same value are averaged
|
|
10
12
|
- CC_ZSCORE(input: np.ndarray[float]): Calculate cross-sectional Z-Score across groups at each time step Z-Score = (x - mean) / stddev, computed across all groups for each time position. NaN values are excluded from mean/stddev computation. NaN input produces NaN output.
|
|
@@ -29,6 +31,7 @@ the `np.ndarray` is `ndarray` type in `numpy` package
|
|
|
29
31
|
- LONGCROSS(a: np.ndarray[float], b: np.ndarray[float], n: int): For 2 arrays A and B, return true if previous N periods A < B, Current A >= B
|
|
30
32
|
- LWMA(input: np.ndarray[float], periods: int): Linear Weighted Moving Average LWMA = SUM(Price * Weight) / SUM(Weight)
|
|
31
33
|
- MA(input: np.ndarray[float], periods: int): Simple Moving Average, also known as arithmetic moving average
|
|
34
|
+
- MAX_DRAWDOWN(input: np.ndarray[float], periods: int): Rolling Maximum Drawdown. MaxDrawdown = minimum peak-to-trough decline within the rolling window. Result is expressed as a negative return (e.g. -0.2 means 20% drawdown from peak). Input should be a price or equity curve series.
|
|
32
35
|
- MIN_MAX_DIFF(input: np.ndarray[float], periods: int): Calculate rolling min-max difference (range) over a moving window TS_MIN_MAX_DIFF = TS_MAX(x, d) - TS_MIN(x, d) Single-pass using two monotonic deques for efficiency.
|
|
33
36
|
- MOMENT(input: np.ndarray[float], periods: int, k: int): Calculate rolling k-th central moment over a moving window MOMENT(x, d, k) = mean((x - mean)^k) over window of d periods. This is the raw (non-adjusted) sample moment. k=2 gives variance (population), k=3 gives raw third moment, etc.
|
|
34
37
|
- NEUTRALIZE(category: np.ndarray[float], input: np.ndarray[float]): Neutralize the effect of a categorical variable on a numeric variable
|
|
@@ -42,6 +45,7 @@ the `np.ndarray` is `ndarray` type in `numpy` package
|
|
|
42
45
|
- RLONGCROSS(a: np.ndarray[float], b: np.ndarray[float], n: int): For 2 arrays A and B, return true if previous N periods A > B, Current A <= B
|
|
43
46
|
- SCAN_ADD(input: np.ndarray[float], condition: np.ndarray[bool]): Conditional cumulative add: r[t] = r[t-1] + (cond[t] ? input[t] : 0) Used for SELF-referencing alpha expressions with additive accumulation. Serial within each stock, parallel across stocks via rayon.
|
|
44
47
|
- SCAN_MUL(input: np.ndarray[float], condition: np.ndarray[bool]): Conditional cumulative multiply: r[t] = r[t-1] * (cond[t] ? input[t] : 1) Used for SELF-referencing alpha expressions like GTJA #143. Serial within each stock, parallel across stocks via rayon.
|
|
48
|
+
- SHARPE(input: np.ndarray[float], periods: int): Rolling Sharpe Ratio of returns. Sharpe = mean(returns) / stddev(returns) Measures risk-adjusted return over a rolling window.
|
|
45
49
|
- SKEWNESS(input: np.ndarray[float], periods: int): Calculate rolling sample Skewness over a moving window Uses adjusted Fisher-Pearson formula (matches pandas): skew = n / ((n-1)(n-2)) * sum(((x-mean)/std)^3) Requires at least 3 valid values.
|
|
46
50
|
- SLOPE(input: np.ndarray[float], periods: int): Linear Regression Slope Calculates the slope of the linear regression line for a moving window.
|
|
47
51
|
- SMA(input: np.ndarray[float], n: int, m: int): Exponential Moving Average (variant of well-known EMA) weight = m / n
|
|
@@ -249,6 +249,9 @@ class ExecContext:
|
|
|
249
249
|
else:
|
|
250
250
|
return alpha.SMA(a, int(args[0]), int(args[1]))
|
|
251
251
|
|
|
252
|
+
def DMA(self, a: np.ndarray, weight: float) -> np.ndarray:
|
|
253
|
+
return alpha.DMA(a, float(weight))
|
|
254
|
+
|
|
252
255
|
# ── TS: Std Dev / Variance ─────────────────────────────────────────
|
|
253
256
|
# BRAIN: ts_std_dev GTJA: STD wq101: STDDEV
|
|
254
257
|
|
|
@@ -315,6 +318,11 @@ class ExecContext:
|
|
|
315
318
|
|
|
316
319
|
TSRANK = TS_RANK # gtja191
|
|
317
320
|
|
|
321
|
+
# ── TS: Quantile ────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
def TS_QUANTILE(self, a: np.ndarray, w: int, q: float) -> np.ndarray:
|
|
324
|
+
return alpha.QUANTILE(a, int(w), q)
|
|
325
|
+
|
|
318
326
|
# ── TS: Delay / Delta ──────────────────────────────────────────────
|
|
319
327
|
# BRAIN: ts_delay, ts_delta
|
|
320
328
|
# wq101: DELAY, DELTA
|
|
@@ -364,6 +372,14 @@ class ExecContext:
|
|
|
364
372
|
|
|
365
373
|
REGRESI = TS_REGRESI # gtja191
|
|
366
374
|
|
|
375
|
+
# ── TS: Slope / Intercept ───────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
def SLOPE(self, a: np.ndarray, w: int) -> np.ndarray:
|
|
378
|
+
return alpha.SLOPE(a, int(w))
|
|
379
|
+
|
|
380
|
+
def INTERCEPT(self, a: np.ndarray, w: int) -> np.ndarray:
|
|
381
|
+
return alpha.INTERCEPT(a, int(w))
|
|
382
|
+
|
|
367
383
|
# ── TS: Counting / Conditional ─────────────────────────────────────
|
|
368
384
|
# GTJA: COUNT, SUMIF
|
|
369
385
|
|
|
@@ -379,6 +395,17 @@ class ExecContext:
|
|
|
379
395
|
|
|
380
396
|
SUMIF = TS_SUMIF # gtja191
|
|
381
397
|
|
|
398
|
+
def SUMBARS(self, a: np.ndarray, amount: float) -> np.ndarray:
|
|
399
|
+
return alpha.SUMBARS(a, amount)
|
|
400
|
+
|
|
401
|
+
# ── TS: Bar Counting ──────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
def BARSLAST(self, cond: np.ndarray) -> np.ndarray:
|
|
404
|
+
return alpha.BARSLAST(np.asarray(cond, dtype=bool))
|
|
405
|
+
|
|
406
|
+
def BARSSINCE(self, cond: np.ndarray) -> np.ndarray:
|
|
407
|
+
return alpha.BARSSINCE(np.asarray(cond, dtype=bool))
|
|
408
|
+
|
|
382
409
|
# ── TS: Conditional Scan (SELF recursion) ───────────────────────────
|
|
383
410
|
# GTJA: SELF-referencing patterns
|
|
384
411
|
|
|
@@ -440,6 +467,16 @@ class ExecContext:
|
|
|
440
467
|
def TS_MOMENT(self, a: np.ndarray, w: int, k: int = 2) -> np.ndarray:
|
|
441
468
|
return alpha.MOMENT(a, int(w), int(k))
|
|
442
469
|
|
|
470
|
+
# ── TS: Binning ────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
def BINS(self, a: np.ndarray, n: int) -> np.ndarray:
|
|
473
|
+
return alpha.BINS(a, int(n))
|
|
474
|
+
|
|
475
|
+
# ── TS: Future Return ──────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
def FRET(self, open: np.ndarray, close: np.ndarray, is_calc: np.ndarray, delay: int, periods: int) -> np.ndarray:
|
|
478
|
+
return alpha.FRET(open, close, is_calc, int(delay), int(periods))
|
|
479
|
+
|
|
443
480
|
# ── TS: Cross Detection ────────────────────────────────────────────
|
|
444
481
|
# AmiBroker/GTJA: CROSS, LONGCROSS
|
|
445
482
|
|
|
@@ -449,6 +486,12 @@ class ExecContext:
|
|
|
449
486
|
def LONGCROSS(self, a: np.ndarray, b: np.ndarray, n: int) -> np.ndarray:
|
|
450
487
|
return alpha.LONGCROSS(a, b, int(n))
|
|
451
488
|
|
|
489
|
+
def RCROSS(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
490
|
+
return alpha.RCROSS(a, b)
|
|
491
|
+
|
|
492
|
+
def RLONGCROSS(self, a: np.ndarray, b: np.ndarray, n: int) -> np.ndarray:
|
|
493
|
+
return alpha.RLONGCROSS(a, b, int(n))
|
|
494
|
+
|
|
452
495
|
# ====================================================================
|
|
453
496
|
# Cross-Sectional Operators (no prefix)
|
|
454
497
|
#
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Copyright 2026 MSD-RS Project LiJia
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
|
|
4
|
+
use num_traits::Float;
|
|
5
|
+
use rayon::prelude::*;
|
|
6
|
+
|
|
7
|
+
use crate::algo::{Context, Error, is_normal};
|
|
8
|
+
|
|
9
|
+
/// Rolling Jensen's Alpha of asset returns against benchmark returns.
|
|
10
|
+
///
|
|
11
|
+
/// Alpha = mean(input) - Beta * mean(benchmark)
|
|
12
|
+
/// Measures excess return of an asset relative to its expected return given beta.
|
|
13
|
+
///
|
|
14
|
+
/// Ref: https://en.wikipedia.org/wiki/Jensen%27s_alpha
|
|
15
|
+
///
|
|
16
|
+
pub fn ta_alpha<NumT: Float + Send + Sync>(
|
|
17
|
+
ctx: &Context,
|
|
18
|
+
r: &mut [NumT],
|
|
19
|
+
input: &[NumT],
|
|
20
|
+
benchmark: &[NumT],
|
|
21
|
+
periods: usize,
|
|
22
|
+
) -> Result<(), Error> {
|
|
23
|
+
if r.len() != input.len() || r.len() != benchmark.len() {
|
|
24
|
+
return Err(Error::LengthMismatch(r.len(), input.len()));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if periods < 2 {
|
|
28
|
+
return Err(Error::InvalidPeriod(format!(
|
|
29
|
+
"alpha requires periods >= 2, got {}",
|
|
30
|
+
periods
|
|
31
|
+
)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
r.par_chunks_mut(ctx.chunk_size(r.len()))
|
|
35
|
+
.zip(input.par_chunks(ctx.chunk_size(input.len())))
|
|
36
|
+
.zip(benchmark.par_chunks(ctx.chunk_size(benchmark.len())))
|
|
37
|
+
.for_each(|((r, x), y)| {
|
|
38
|
+
let start = ctx.start(r.len());
|
|
39
|
+
let end = ctx.end(r.len());
|
|
40
|
+
r.fill(NumT::nan());
|
|
41
|
+
|
|
42
|
+
if ctx.is_skip_nan() {
|
|
43
|
+
for i in start..end {
|
|
44
|
+
let mut sum_x = NumT::zero();
|
|
45
|
+
let mut sum_y = NumT::zero();
|
|
46
|
+
let mut sum_xy = NumT::zero();
|
|
47
|
+
let mut sum_y2 = NumT::zero();
|
|
48
|
+
let mut n = 0usize;
|
|
49
|
+
let mut gap = false;
|
|
50
|
+
|
|
51
|
+
let mut j = i;
|
|
52
|
+
loop {
|
|
53
|
+
if is_normal(&x[j]) && is_normal(&y[j]) {
|
|
54
|
+
sum_x = sum_x + x[j];
|
|
55
|
+
sum_y = sum_y + y[j];
|
|
56
|
+
sum_xy = sum_xy + x[j] * y[j];
|
|
57
|
+
sum_y2 = sum_y2 + y[j] * y[j];
|
|
58
|
+
n += 1;
|
|
59
|
+
if n == periods {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
gap = true;
|
|
64
|
+
}
|
|
65
|
+
if j == 0 {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
j -= 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if !is_normal(&x[i]) || !is_normal(&y[i]) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if ctx.is_strictly_cycle() {
|
|
76
|
+
if n != periods || gap || (i - j + 1) != periods {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if n < 2 {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let nf = NumT::from(n).unwrap();
|
|
86
|
+
let denom = nf * sum_y2 - sum_y * sum_y;
|
|
87
|
+
if denom == NumT::zero() {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
let beta = (nf * sum_xy - sum_x * sum_y) / denom;
|
|
91
|
+
r[i] = (sum_x - beta * sum_y) / nf;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
let mut sum_x = NumT::zero();
|
|
95
|
+
let mut sum_y = NumT::zero();
|
|
96
|
+
let mut sum_xy = NumT::zero();
|
|
97
|
+
let mut sum_y2 = NumT::zero();
|
|
98
|
+
let mut nan_in_window = 0usize;
|
|
99
|
+
|
|
100
|
+
let pre_start = if start >= periods { start - periods } else { 0 };
|
|
101
|
+
for k in pre_start..start {
|
|
102
|
+
if is_normal(&x[k]) && is_normal(&y[k]) {
|
|
103
|
+
sum_x = sum_x + x[k];
|
|
104
|
+
sum_y = sum_y + y[k];
|
|
105
|
+
sum_xy = sum_xy + x[k] * y[k];
|
|
106
|
+
sum_y2 = sum_y2 + y[k] * y[k];
|
|
107
|
+
} else {
|
|
108
|
+
nan_in_window += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for i in start..end {
|
|
113
|
+
if is_normal(&x[i]) && is_normal(&y[i]) {
|
|
114
|
+
sum_x = sum_x + x[i];
|
|
115
|
+
sum_y = sum_y + y[i];
|
|
116
|
+
sum_xy = sum_xy + x[i] * y[i];
|
|
117
|
+
sum_y2 = sum_y2 + y[i] * y[i];
|
|
118
|
+
} else {
|
|
119
|
+
nan_in_window += 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if i >= periods {
|
|
123
|
+
let old_x = x[i - periods];
|
|
124
|
+
let old_y = y[i - periods];
|
|
125
|
+
if is_normal(&old_x) && is_normal(&old_y) {
|
|
126
|
+
sum_x = sum_x - old_x;
|
|
127
|
+
sum_y = sum_y - old_y;
|
|
128
|
+
sum_xy = sum_xy - old_x * old_y;
|
|
129
|
+
sum_y2 = sum_y2 - old_y * old_y;
|
|
130
|
+
} else {
|
|
131
|
+
nan_in_window -= 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if !is_normal(&x[i]) || !is_normal(&y[i]) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if ctx.is_strictly_cycle() && i < periods - 1 {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if nan_in_window > 0 {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let n = if i < periods { i + 1 } else { periods };
|
|
148
|
+
if n < 2 {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
let nf = NumT::from(n).unwrap();
|
|
152
|
+
let denom = nf * sum_y2 - sum_y * sum_y;
|
|
153
|
+
if denom == NumT::zero() {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let beta = (nf * sum_xy - sum_x * sum_y) / denom;
|
|
157
|
+
r[i] = (sum_x - beta * sum_y) / nf;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
Ok(())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[cfg(test)]
|
|
166
|
+
mod tests {
|
|
167
|
+
use crate::algo::{
|
|
168
|
+
assert_vec_eq_nan,
|
|
169
|
+
context::{FLAG_SKIP_NAN, FLAG_STRICTLY_CYCLE},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
use super::*;
|
|
173
|
+
|
|
174
|
+
#[test]
|
|
175
|
+
fn test_ta_alpha_basic() {
|
|
176
|
+
// y = 2x, Alpha should be 0 (returns proportional, no excess return)
|
|
177
|
+
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
|
178
|
+
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
|
|
179
|
+
let periods = 3;
|
|
180
|
+
let mut r = vec![0.0; x.len()];
|
|
181
|
+
let ctx = Context::new(0, 0, 0);
|
|
182
|
+
ta_alpha(&ctx, &mut r, &x, &y, periods).unwrap();
|
|
183
|
+
assert_vec_eq_nan(&r, &vec![f64::NAN, 0.0, 0.0, 0.0, 0.0]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn test_ta_alpha_positive() {
|
|
188
|
+
// x has extra 1.0 above y*0.5
|
|
189
|
+
let x = vec![2.0, 3.0, 4.0, 5.0, 6.0]; // y*0.5 + 1
|
|
190
|
+
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0]; // y=2*(x-1)
|
|
191
|
+
let periods = 3;
|
|
192
|
+
let mut r = vec![0.0; x.len()];
|
|
193
|
+
let ctx = Context::new(0, 0, 0);
|
|
194
|
+
ta_alpha(&ctx, &mut r, &x, &y, periods).unwrap();
|
|
195
|
+
// Alpha should be 1.0
|
|
196
|
+
assert_vec_eq_nan(&r, &vec![f64::NAN, 1.0, 1.0, 1.0, 1.0]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[test]
|
|
200
|
+
fn test_ta_alpha_strictly_cycle() {
|
|
201
|
+
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
|
202
|
+
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
|
|
203
|
+
let periods = 3;
|
|
204
|
+
let mut r = vec![0.0; x.len()];
|
|
205
|
+
let ctx = Context::new(0, 0, FLAG_STRICTLY_CYCLE);
|
|
206
|
+
ta_alpha(&ctx, &mut r, &x, &y, periods).unwrap();
|
|
207
|
+
assert_vec_eq_nan(&r, &vec![f64::NAN, f64::NAN, 0.0, 0.0, 0.0]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[test]
|
|
211
|
+
fn test_ta_alpha_skip_nan() {
|
|
212
|
+
let x = vec![1.0, f64::NAN, 3.0, 4.0, 5.0];
|
|
213
|
+
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
|
|
214
|
+
let periods = 3;
|
|
215
|
+
let mut r = vec![0.0; x.len()];
|
|
216
|
+
|
|
217
|
+
let ctx = Context::new(0, 0, FLAG_SKIP_NAN);
|
|
218
|
+
ta_alpha(&ctx, &mut r, &x, &y, periods).unwrap();
|
|
219
|
+
// i=0: n=1 → NaN
|
|
220
|
+
// i=1: NaN → skip
|
|
221
|
+
// i=2: pairs (0,0) and (2,2) valid, n=2 → Alpha=0
|
|
222
|
+
// i=3: 3 valid pairs → Alpha=0
|
|
223
|
+
assert_vec_eq_nan(&r, &vec![f64::NAN, f64::NAN, 0.0, 0.0, 0.0]);
|
|
224
|
+
}
|
|
225
|
+
}
|