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.
Files changed (81) hide show
  1. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/CHANGELOG.md +17 -0
  2. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/Cargo.lock +1 -1
  3. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/Cargo.toml +2 -1
  4. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/PKG-INFO +5 -1
  5. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/README.md +4 -0
  6. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo/algo_gen.py +93 -0
  7. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo.md +4 -0
  8. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/context.py +43 -0
  9. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/to_python.py +1 -1
  10. py_alpha_lib-0.2.4/src/algo/alpha.rs +225 -0
  11. py_alpha_lib-0.2.4/src/algo/beta.rs +261 -0
  12. py_alpha_lib-0.2.4/src/algo/drawdown.rs +225 -0
  13. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/mod.rs +8 -0
  14. py_alpha_lib-0.2.4/src/algo/sharpe.rs +245 -0
  15. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.agent/skills/add_algo/SKILL.md +0 -0
  16. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.github/workflows/CI.yml +0 -0
  17. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.gitignore +0 -0
  18. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/.nwa-config.yaml +0 -0
  19. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/LICENSE +0 -0
  20. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/articles/001.md +0 -0
  21. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/articles/COMPARISON.md +0 -0
  22. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/benchmarks/benchmark_alpha101.py +0 -0
  23. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/benchmarks/benchmark_pandas.py +0 -0
  24. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/benchmarks/benchmark_polars_ta.py +0 -0
  25. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/build.rs +0 -0
  26. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/al/__init__.py +0 -0
  27. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/al/alpha191.py +0 -0
  28. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/al/alpha191_context.py +0 -0
  29. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/alpha191.txt +0 -0
  30. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/gtja191/main.py +0 -0
  31. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/al/__init__.py +0 -0
  32. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/al/alpha101.py +0 -0
  33. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/al/alpha101_context.py +0 -0
  34. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/alpha101.txt +0 -0
  35. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/main.py +0 -0
  36. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pd_/__init__.py +0 -0
  37. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pd_/alpha101_adjusted.py +0 -0
  38. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pl_/__init__.py +0 -0
  39. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/pl_/alpha101_adjusted.py +0 -0
  40. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/examples/wq101/result.md +0 -0
  41. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/pyproject.toml +0 -0
  42. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/__init__.py +0 -0
  43. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo/__init__.py +0 -0
  44. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/algo/algo.py +0 -0
  45. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/__init__.py +0 -0
  46. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/__main__.py +0 -0
  47. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/alpha.lark +0 -0
  48. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/alpha/lang/parser.py +0 -0
  49. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/conftest.py +0 -0
  50. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_grammar.py +0 -0
  51. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_rank.py +0 -0
  52. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_talib.py +0 -0
  53. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/python/tests/test_to_python.py +0 -0
  54. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/rustfmt.toml +0 -0
  55. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/backfill.rs +0 -0
  56. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/context.rs +0 -0
  57. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/cross.rs +0 -0
  58. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/ema.rs +0 -0
  59. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/entropy.rs +0 -0
  60. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/error.rs +0 -0
  61. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/extremum.rs +0 -0
  62. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/group.rs +0 -0
  63. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/ma.rs +0 -0
  64. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/misc.rs +0 -0
  65. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/moments.rs +0 -0
  66. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/neutralize.rs +0 -0
  67. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/quantile.rs +0 -0
  68. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/rank.rs +0 -0
  69. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/returns.rs +0 -0
  70. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/scan.rs +0 -0
  71. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/series.rs +0 -0
  72. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/skip_nan_window.rs +0 -0
  73. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/slope.rs +0 -0
  74. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/stats.rs +0 -0
  75. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/stddev.rs +0 -0
  76. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/sum.rs +0 -0
  77. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/algo/zscore.rs +0 -0
  78. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/src/lib.rs +0 -0
  79. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/tests/rank.py +0 -0
  80. {py_alpha_lib-0.2.2 → py_alpha_lib-0.2.4}/tests/usage.py +0 -0
  81. {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
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "alpha"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  dependencies = [
9
9
  "anyhow",
10
10
  "log",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "alpha"
3
- version = "0.2.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.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
  #
@@ -119,7 +119,7 @@ class AlphaTransformer(Transformer):
119
119
  for arg in args:
120
120
  try:
121
121
  val = float(arg)
122
- if '.' in arg and val == val:
122
+ if '.' in arg and val == arg:
123
123
  arg = str(round(val))
124
124
  except (ValueError, TypeError):
125
125
  pass
@@ -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
+ }