py-fastplus 0.2.1__tar.gz → 0.3.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 (28) hide show
  1. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/.github/workflows/Publish.yml +7 -2
  2. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/Cargo.lock +1 -1
  3. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/Cargo.toml +1 -1
  4. py_fastplus-0.3.0/PKG-INFO +286 -0
  5. py_fastplus-0.3.0/README.md +274 -0
  6. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/python/fastplus/core.pyi +2 -0
  7. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/python/tests/test_all.py +11 -0
  8. py_fastplus-0.3.0/src/ast.rs +279 -0
  9. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/src/operator.rs +1 -1
  10. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/src/parser.rs +40 -6
  11. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/src/python.rs +10 -1
  12. py_fastplus-0.2.1/PKG-INFO +0 -32
  13. py_fastplus-0.2.1/README.md +0 -20
  14. py_fastplus-0.2.1/src/ast.rs +0 -129
  15. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/.githooks/pre-commit +0 -0
  16. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/.github/workflows/Commit.yml +0 -0
  17. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/.gitignore +0 -0
  18. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/LICENSE +0 -0
  19. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/examples/parse_demo.rs +0 -0
  20. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/pyproject.toml +0 -0
  21. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/python/fastplus/__about__.py +0 -0
  22. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/python/fastplus/__init__.py +0 -0
  23. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/python/fastplus/parser.py +0 -0
  24. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/src/fastplus.pest +0 -0
  25. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/src/field.rs +0 -0
  26. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/src/lib.rs +0 -0
  27. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/src/python_macro.rs +0 -0
  28. {py_fastplus-0.2.1 → py_fastplus-0.3.0}/uv.lock +0 -0
@@ -187,12 +187,17 @@ jobs:
187
187
  uses: actions/attest@v4
188
188
  with:
189
189
  subject-path: 'wheels-*/*'
190
+ - name: Recreate latest release
191
+ if: ${{ steps.release-tag.outputs.tag == 'dev-latest' || steps.release-tag.outputs.tag == 'main-latest' }}
192
+ env:
193
+ GH_TOKEN: ${{ github.token }}
194
+ RELEASE_TAG: ${{ steps.release-tag.outputs.tag }}
195
+ run: gh release delete "$RELEASE_TAG" --yes || true
190
196
  - name: Create or update GitHub Release
191
197
  uses: softprops/action-gh-release@v3
192
198
  with:
193
199
  tag_name: ${{ steps.release-tag.outputs.tag }}
194
- body: FastPlus release generated by the Publish workflow.
195
- generate_release_notes: false
200
+ generate_release_notes: true
196
201
  append_body: false
197
202
  overwrite_files: true
198
203
  files: wheels-*/*
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "fastplus"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  dependencies = [
9
9
  "pest",
10
10
  "pest_derive",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fastplus"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-fastplus
3
+ Version: 0.3.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ License-File: LICENSE
7
+ Summary: Add your description here
8
+ Author-email: AshSwing <ashswing@email.cn>
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
11
+
12
+ # FastPlus
13
+
14
+ FastPlus 是一个基于 Rust 和 PyO3 实现的 WorldQuant Fast Expression 解析器。它把 Alpha 表达式解析为可访问的 Python 对象,并在解析已知操作符时进行参数数量、参数类型和部分操作符专属规则检查。
15
+
16
+ 当前实现面向 RegularAlpha,收录了 Expert 权限范围内的 108 个操作符。
17
+
18
+ ## 安装
19
+
20
+ FastPlus 需要 Python 3.12 及以上版本。
21
+
22
+ 从 PyPI 安装:
23
+
24
+ ```bash
25
+ pip install py-fastplus
26
+ ```
27
+
28
+ 如果需要从源码开发:
29
+
30
+ ```bash
31
+ uv sync
32
+ source .venv/bin/activate
33
+ maturin develop --skip-install
34
+ ```
35
+
36
+ 安装后可以在 Python 中导入:
37
+
38
+ ```python
39
+ import fastplus
40
+ ```
41
+
42
+ ## Python 用法
43
+
44
+ 使用 `fastplus.parse` 解析一个 Alpha 表达式:
45
+
46
+ ```python
47
+ import fastplus
48
+
49
+ alpha = fastplus.parse("a=ts_delay(close, 5); group_rank(a, industry)")
50
+
51
+ print(alpha.fields)
52
+ print(alpha.operators)
53
+ ```
54
+
55
+ 输出:
56
+
57
+ ```text
58
+ {'matrix': ['close'], 'vector': [], 'group': ['industry']}
59
+ ['ts_delay', 'group_rank']
60
+ ```
61
+
62
+ `Alpha` 提供以下属性:
63
+
64
+ - `assignments`:赋值语句列表,每个元素包含 `variable` 和 `value`。
65
+ - `signal`:最终信号表达式。
66
+ - `fields`:按 `matrix`、`vector`、`group` 分类的数据字段名称。
67
+ - `operators`:表达式中使用的操作符名称列表。
68
+
69
+ 解析失败会抛出 `ValueError`。例如:
70
+
71
+ 最终 `signal` 不能是 `Group` 或 `Vector`:
72
+
73
+ ```python
74
+ fastplus.parse("bucket(rank(x), range='0, 1, 0.1')")
75
+ ```
76
+
77
+ ```text
78
+ --> 1:1
79
+ |
80
+ 1 | bucket(rank(x), range='0, 1, 0.1')
81
+ | ^--------------------------------^
82
+ |
83
+ = signal must be Matrix-compatible, but got Group
84
+ ```
85
+
86
+ `bucket` 不能同时提供有效的 `range` 和 `buckets`:
87
+
88
+ ```python
89
+ fastplus.parse("bucket(rank(x), range='0, 1, 0.1', buckets='0, 0.5, 1')")
90
+ ```
91
+
92
+ ```text
93
+ --> 1:1
94
+ |
95
+ 1 | bucket(rank(x), range='0, 1, 0.1', buckets='0, 0.5, 1')
96
+ | ^-----------------------------------------------------^
97
+ |
98
+ = operator `bucket` check failed: InvalidExpression("bucket requires exactly one of range and buckets to be valid")
99
+ ```
100
+
101
+ `vec_*` 操作符要求 `Vector`,不能把其他返回 `Matrix` 的操作嵌进去:
102
+
103
+ ```python
104
+ fastplus.parse("vec_avg(rank(x))")
105
+ ```
106
+
107
+ ```text
108
+ --> 1:1
109
+ |
110
+ 1 | vec_avg(rank(x))
111
+ | ^--------------^
112
+ |
113
+ = operator `vec_avg` check failed: InvalidArgumentType { expected: Vector, actual: Matrix }
114
+ ```
115
+
116
+ 异常信息会同时指出错误位置、操作符名称、错误类型、期望类型和实际类型,便于研究员和大模型定位问题并自动修复表达式。
117
+
118
+ ## FastPlus 语法
119
+
120
+ ### Alpha 和赋值
121
+
122
+ 一个 Alpha 由零个或多个赋值语句和一个最终信号组成:
123
+
124
+ ```text
125
+ [variable = expression;] expression [;]
126
+ ```
127
+
128
+ 例如:
129
+
130
+ ```text
131
+ adv = ts_mean(volume, 20);
132
+ signal = rank(adv);
133
+ signal
134
+ ```
135
+
136
+ 赋值左侧只能是普通变量,不能使用占位符。最后一个分号可选。
137
+
138
+ ### 变量、字段和占位符
139
+
140
+ 普通变量由字母开头,只能包含 ASCII 字母、数字和下划线:
141
+
142
+ ```text
143
+ close
144
+ industry
145
+ my_signal_1
146
+ ```
147
+
148
+ 占位符必须使用完整形式:
149
+
150
+ ```text
151
+ <close_field/>
152
+ ```
153
+
154
+ 目前不支持常数占位符,例如 `<1/>` 或 `<0.5/>`。占位符会统一作为数据字段处理,并在 `Alpha.fields` 中保留完整的 `<`、`/>` 标记。
155
+
156
+ 已知操作符会根据参数签名把变量和占位符分类为 `matrix`、`vector` 或 `group`。未知变量在无法确定类型时作为通配类型处理;赋值左侧定义的变量不会被计入 `fields`。
157
+
158
+ ### 操作符调用
159
+
160
+ 操作符调用格式为:
161
+
162
+ ```text
163
+ operator(positional_arg, ..., keyword=value, ...)
164
+ ```
165
+
166
+ 位置参数必须写在关键字参数之前,关键字参数只能使用常量:
167
+
168
+ ```text
169
+ add(close, volume, filter=true)
170
+ clamp(x, lower=-1, upper=1, inverse=false, mask='mean')
171
+ ```
172
+
173
+ 已知操作符会检查:
174
+
175
+ - 位置参数数量;
176
+ - 位置参数类型;
177
+ - 关键字名称和类型;
178
+ - 必填关键字参数;
179
+ - 部分操作符专属约束,例如 `k <= d`、`clamp` 的上下界关系,以及 `bucket` 的参数互斥和数组顺序。
180
+
181
+ 未知操作符会保留在表达式中,但跳过操作符检查。
182
+
183
+ 当前实现中的几个平台差异:
184
+
185
+ - `subtract` 目前只支持两个位置参数。
186
+ - `add`、`multiply`、`max`、`min` 等可变参数操作符至少需要两个位置参数。
187
+ - `jump_decay` 的位置参数是 `x`、`d`,关键字参数是 `stddev`、`sensitivity`、`force`。
188
+ - `ts_rank_gmean_amean_diff` 使用必填的 `lookback` 关键字参数。
189
+ - 其他包含 lookback 语义的时间序列操作符通常把 lookback 作为最后一个位置参数,例如 `ts_backfill(x, d)` 和 `ts_rank(x, d)`。
190
+ - 关键字参数只能使用常量,不能传入变量、占位符或复杂表达式。
191
+ - 操作符至少需要一个位置参数;位置参数和关键字参数不能交错。
192
+
193
+ ### 表达式和运算符
194
+
195
+ 支持括号、前缀运算、二元运算和三元表达式:
196
+
197
+ ```text
198
+ -(x + 1)
199
+ x > 0 && y < 1
200
+ x ? y : z
201
+ ```
202
+
203
+ 支持的二元运算符包括:
204
+
205
+ ```text
206
+ || && != == >= <= > < + - * /
207
+ ```
208
+
209
+ 支持的前缀运算符包括:
210
+
211
+ ```text
212
+ ! + -
213
+ ```
214
+
215
+ 三元表达式的三个分支都必须是完整表达式。
216
+
217
+ ### 常量和关键字参数
218
+
219
+ 支持整数、浮点数、布尔值和字符串常量。数值常量会根据值归类为整数、正整数、比例、浮点数等类型;数值标量可以按平台规则广播为 `Matrix`。
220
+
221
+ 字符串关键字会经过统一解析,例如:
222
+
223
+ ```text
224
+ driver='gaussian'
225
+ mask='nearest_bound'
226
+ mask='mean'
227
+ ignore='NaN'
228
+ ```
229
+
230
+ 还支持范围、数组和集合形式,例如:
231
+
232
+ ```text
233
+ range='0, 1, 0.1'
234
+ buckets='0, 0.2, 0.5, 1'
235
+ ignore='NaN 0 1'
236
+ ```
237
+
238
+ ### 注释和空白
239
+
240
+ 空格、制表符和换行会被忽略。支持以下注释:
241
+
242
+ ```text
243
+ // 行注释
244
+ # 行注释
245
+ /* 块注释 */
246
+ ```
247
+
248
+ ## 当前限制和待开发功能
249
+
250
+ ### 操作符权限范围
251
+
252
+ 当前只有 Expert 权限,因此只能完整检查 Expert 范围内的操作符。未知操作符会跳过检查,以便后续扩展;欢迎提交 Pull Request 补充操作符签名和规则。
253
+
254
+ ### 当前不会检查的规则
255
+
256
+ 以下表达式按平台规则应该失败,但当前还没有检测:
257
+
258
+ - 定义了但没有使用的变量;
259
+ - 用户定义变量与数据字段重名。
260
+ - 字段分类的交叉校验:如果同一个字段同时出现在 `matrix` 和 `vector`(或其他类别)中,说明至少有一个位置的类型推断错误,目前尚未检测。
261
+
262
+ ### 待开发功能
263
+
264
+ - 模板展开:根据占位符自动生成具体表达式。
265
+ - 参数遍历:遍历参数空间并生成参数组合。
266
+ - AST 结构化输出:提供 `.format()` 方法。
267
+ - 表达式编译:提供 `.compile()` 方法,自动合并同类项并优化因子表达式结构。
268
+ - 补充更多权限级别的操作符和操作符专属校验。
269
+
270
+ ## 开发验证
271
+
272
+ Rust 测试和检查:
273
+
274
+ ```bash
275
+ cargo fmt -- --check
276
+ cargo test --lib
277
+ cargo clippy --all-targets --all-features -- -D warnings
278
+ ```
279
+
280
+ Python 测试和类型检查:
281
+
282
+ ```bash
283
+ uv run pytest
284
+ uv run ty check
285
+ ```
286
+
@@ -0,0 +1,274 @@
1
+ # FastPlus
2
+
3
+ FastPlus 是一个基于 Rust 和 PyO3 实现的 WorldQuant Fast Expression 解析器。它把 Alpha 表达式解析为可访问的 Python 对象,并在解析已知操作符时进行参数数量、参数类型和部分操作符专属规则检查。
4
+
5
+ 当前实现面向 RegularAlpha,收录了 Expert 权限范围内的 108 个操作符。
6
+
7
+ ## 安装
8
+
9
+ FastPlus 需要 Python 3.12 及以上版本。
10
+
11
+ 从 PyPI 安装:
12
+
13
+ ```bash
14
+ pip install py-fastplus
15
+ ```
16
+
17
+ 如果需要从源码开发:
18
+
19
+ ```bash
20
+ uv sync
21
+ source .venv/bin/activate
22
+ maturin develop --skip-install
23
+ ```
24
+
25
+ 安装后可以在 Python 中导入:
26
+
27
+ ```python
28
+ import fastplus
29
+ ```
30
+
31
+ ## Python 用法
32
+
33
+ 使用 `fastplus.parse` 解析一个 Alpha 表达式:
34
+
35
+ ```python
36
+ import fastplus
37
+
38
+ alpha = fastplus.parse("a=ts_delay(close, 5); group_rank(a, industry)")
39
+
40
+ print(alpha.fields)
41
+ print(alpha.operators)
42
+ ```
43
+
44
+ 输出:
45
+
46
+ ```text
47
+ {'matrix': ['close'], 'vector': [], 'group': ['industry']}
48
+ ['ts_delay', 'group_rank']
49
+ ```
50
+
51
+ `Alpha` 提供以下属性:
52
+
53
+ - `assignments`:赋值语句列表,每个元素包含 `variable` 和 `value`。
54
+ - `signal`:最终信号表达式。
55
+ - `fields`:按 `matrix`、`vector`、`group` 分类的数据字段名称。
56
+ - `operators`:表达式中使用的操作符名称列表。
57
+
58
+ 解析失败会抛出 `ValueError`。例如:
59
+
60
+ 最终 `signal` 不能是 `Group` 或 `Vector`:
61
+
62
+ ```python
63
+ fastplus.parse("bucket(rank(x), range='0, 1, 0.1')")
64
+ ```
65
+
66
+ ```text
67
+ --> 1:1
68
+ |
69
+ 1 | bucket(rank(x), range='0, 1, 0.1')
70
+ | ^--------------------------------^
71
+ |
72
+ = signal must be Matrix-compatible, but got Group
73
+ ```
74
+
75
+ `bucket` 不能同时提供有效的 `range` 和 `buckets`:
76
+
77
+ ```python
78
+ fastplus.parse("bucket(rank(x), range='0, 1, 0.1', buckets='0, 0.5, 1')")
79
+ ```
80
+
81
+ ```text
82
+ --> 1:1
83
+ |
84
+ 1 | bucket(rank(x), range='0, 1, 0.1', buckets='0, 0.5, 1')
85
+ | ^-----------------------------------------------------^
86
+ |
87
+ = operator `bucket` check failed: InvalidExpression("bucket requires exactly one of range and buckets to be valid")
88
+ ```
89
+
90
+ `vec_*` 操作符要求 `Vector`,不能把其他返回 `Matrix` 的操作嵌进去:
91
+
92
+ ```python
93
+ fastplus.parse("vec_avg(rank(x))")
94
+ ```
95
+
96
+ ```text
97
+ --> 1:1
98
+ |
99
+ 1 | vec_avg(rank(x))
100
+ | ^--------------^
101
+ |
102
+ = operator `vec_avg` check failed: InvalidArgumentType { expected: Vector, actual: Matrix }
103
+ ```
104
+
105
+ 异常信息会同时指出错误位置、操作符名称、错误类型、期望类型和实际类型,便于研究员和大模型定位问题并自动修复表达式。
106
+
107
+ ## FastPlus 语法
108
+
109
+ ### Alpha 和赋值
110
+
111
+ 一个 Alpha 由零个或多个赋值语句和一个最终信号组成:
112
+
113
+ ```text
114
+ [variable = expression;] expression [;]
115
+ ```
116
+
117
+ 例如:
118
+
119
+ ```text
120
+ adv = ts_mean(volume, 20);
121
+ signal = rank(adv);
122
+ signal
123
+ ```
124
+
125
+ 赋值左侧只能是普通变量,不能使用占位符。最后一个分号可选。
126
+
127
+ ### 变量、字段和占位符
128
+
129
+ 普通变量由字母开头,只能包含 ASCII 字母、数字和下划线:
130
+
131
+ ```text
132
+ close
133
+ industry
134
+ my_signal_1
135
+ ```
136
+
137
+ 占位符必须使用完整形式:
138
+
139
+ ```text
140
+ <close_field/>
141
+ ```
142
+
143
+ 目前不支持常数占位符,例如 `<1/>` 或 `<0.5/>`。占位符会统一作为数据字段处理,并在 `Alpha.fields` 中保留完整的 `<`、`/>` 标记。
144
+
145
+ 已知操作符会根据参数签名把变量和占位符分类为 `matrix`、`vector` 或 `group`。未知变量在无法确定类型时作为通配类型处理;赋值左侧定义的变量不会被计入 `fields`。
146
+
147
+ ### 操作符调用
148
+
149
+ 操作符调用格式为:
150
+
151
+ ```text
152
+ operator(positional_arg, ..., keyword=value, ...)
153
+ ```
154
+
155
+ 位置参数必须写在关键字参数之前,关键字参数只能使用常量:
156
+
157
+ ```text
158
+ add(close, volume, filter=true)
159
+ clamp(x, lower=-1, upper=1, inverse=false, mask='mean')
160
+ ```
161
+
162
+ 已知操作符会检查:
163
+
164
+ - 位置参数数量;
165
+ - 位置参数类型;
166
+ - 关键字名称和类型;
167
+ - 必填关键字参数;
168
+ - 部分操作符专属约束,例如 `k <= d`、`clamp` 的上下界关系,以及 `bucket` 的参数互斥和数组顺序。
169
+
170
+ 未知操作符会保留在表达式中,但跳过操作符检查。
171
+
172
+ 当前实现中的几个平台差异:
173
+
174
+ - `subtract` 目前只支持两个位置参数。
175
+ - `add`、`multiply`、`max`、`min` 等可变参数操作符至少需要两个位置参数。
176
+ - `jump_decay` 的位置参数是 `x`、`d`,关键字参数是 `stddev`、`sensitivity`、`force`。
177
+ - `ts_rank_gmean_amean_diff` 使用必填的 `lookback` 关键字参数。
178
+ - 其他包含 lookback 语义的时间序列操作符通常把 lookback 作为最后一个位置参数,例如 `ts_backfill(x, d)` 和 `ts_rank(x, d)`。
179
+ - 关键字参数只能使用常量,不能传入变量、占位符或复杂表达式。
180
+ - 操作符至少需要一个位置参数;位置参数和关键字参数不能交错。
181
+
182
+ ### 表达式和运算符
183
+
184
+ 支持括号、前缀运算、二元运算和三元表达式:
185
+
186
+ ```text
187
+ -(x + 1)
188
+ x > 0 && y < 1
189
+ x ? y : z
190
+ ```
191
+
192
+ 支持的二元运算符包括:
193
+
194
+ ```text
195
+ || && != == >= <= > < + - * /
196
+ ```
197
+
198
+ 支持的前缀运算符包括:
199
+
200
+ ```text
201
+ ! + -
202
+ ```
203
+
204
+ 三元表达式的三个分支都必须是完整表达式。
205
+
206
+ ### 常量和关键字参数
207
+
208
+ 支持整数、浮点数、布尔值和字符串常量。数值常量会根据值归类为整数、正整数、比例、浮点数等类型;数值标量可以按平台规则广播为 `Matrix`。
209
+
210
+ 字符串关键字会经过统一解析,例如:
211
+
212
+ ```text
213
+ driver='gaussian'
214
+ mask='nearest_bound'
215
+ mask='mean'
216
+ ignore='NaN'
217
+ ```
218
+
219
+ 还支持范围、数组和集合形式,例如:
220
+
221
+ ```text
222
+ range='0, 1, 0.1'
223
+ buckets='0, 0.2, 0.5, 1'
224
+ ignore='NaN 0 1'
225
+ ```
226
+
227
+ ### 注释和空白
228
+
229
+ 空格、制表符和换行会被忽略。支持以下注释:
230
+
231
+ ```text
232
+ // 行注释
233
+ # 行注释
234
+ /* 块注释 */
235
+ ```
236
+
237
+ ## 当前限制和待开发功能
238
+
239
+ ### 操作符权限范围
240
+
241
+ 当前只有 Expert 权限,因此只能完整检查 Expert 范围内的操作符。未知操作符会跳过检查,以便后续扩展;欢迎提交 Pull Request 补充操作符签名和规则。
242
+
243
+ ### 当前不会检查的规则
244
+
245
+ 以下表达式按平台规则应该失败,但当前还没有检测:
246
+
247
+ - 定义了但没有使用的变量;
248
+ - 用户定义变量与数据字段重名。
249
+ - 字段分类的交叉校验:如果同一个字段同时出现在 `matrix` 和 `vector`(或其他类别)中,说明至少有一个位置的类型推断错误,目前尚未检测。
250
+
251
+ ### 待开发功能
252
+
253
+ - 模板展开:根据占位符自动生成具体表达式。
254
+ - 参数遍历:遍历参数空间并生成参数组合。
255
+ - AST 结构化输出:提供 `.format()` 方法。
256
+ - 表达式编译:提供 `.compile()` 方法,自动合并同类项并优化因子表达式结构。
257
+ - 补充更多权限级别的操作符和操作符专属校验。
258
+
259
+ ## 开发验证
260
+
261
+ Rust 测试和检查:
262
+
263
+ ```bash
264
+ cargo fmt -- --check
265
+ cargo test --lib
266
+ cargo clippy --all-targets --all-features -- -D warnings
267
+ ```
268
+
269
+ Python 测试和类型检查:
270
+
271
+ ```bash
272
+ uv run pytest
273
+ uv run ty check
274
+ ```
@@ -114,5 +114,7 @@ class Assignment:
114
114
  class Alpha:
115
115
  assignments: list[Assignment]
116
116
  signal: Expression
117
+ fields: dict[str, list[str]]
118
+ operators: list[str]
117
119
 
118
120
  def parse(input: str) -> Alpha: ...
@@ -73,3 +73,14 @@ def test_parse_tree_exposes_expression_and_constant_types():
73
73
  def test_python_parse_reports_operator_type_errors():
74
74
  with pytest.raises(ValueError, match="divide"):
75
75
  fastplus.parse("divide('not a matrix', 2)")
76
+
77
+
78
+ def test_alpha_reports_typed_fields_and_operators():
79
+ alpha = fastplus.parse("a=ts_delay(close, 5);group_rank(a, industry)")
80
+
81
+ assert alpha.fields == {
82
+ "matrix": ["close"],
83
+ "vector": [],
84
+ "group": ["industry"],
85
+ }
86
+ assert alpha.operators == ["ts_delay", "group_rank"]
@@ -0,0 +1,279 @@
1
+ //! AST
2
+ //! 唯一结果对象: Alpha
3
+ //! |-> [Assignment;] 赋值语句
4
+ //! |-> [Assignment;] 赋值语句
5
+ //! |-> [...;] ...
6
+ //! |-> signal(Expression) 信号表达式
7
+ //! Alpha 由 N 个赋值语句和 1 个信号表达式组成
8
+ //!
9
+ //! Assignment 赋值语句
10
+ //! |-> variable(String) = value(Expression);
11
+ //!
12
+ //! 核心结构: Expression 表达式
13
+ //! |-> Group(Expression) / Ternary / Binary / Unary 递归结构
14
+ //! |-> Variable / Placeholder 占位结构
15
+ //! |-> Operation* 操作符*
16
+ //! |-> Constant* 常数项*
17
+ //!
18
+ //! 特殊规则:
19
+ //! - 不可以给常数赋值 ❌ -> 刚试了下又可以了(2026-07-28)
20
+ //! - 不可复用数据字段名 -> 非左值 Variable 都是数据字段
21
+ //! - 参数支持字符串/字面值 -> 自动转为字符串解析
22
+ //! - 支持 NaN, 不支持 Inf
23
+
24
+ use std::collections::HashMap;
25
+ use std::collections::HashSet;
26
+
27
+ use crate::field::Constant;
28
+ use crate::field::Field;
29
+ use crate::operator::get_operator;
30
+
31
+ use pest_derive::Parser;
32
+
33
+ #[derive(Parser)]
34
+ #[grammar = "fastplus.pest"]
35
+ pub struct FastPlusParser;
36
+
37
+ #[derive(Debug, Clone)]
38
+ pub struct Alpha {
39
+ assignments: Vec<Assignment>,
40
+ signal: Expression,
41
+ }
42
+
43
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
44
+ pub struct AlphaFields {
45
+ pub matrix: Vec<String>,
46
+ pub vector: Vec<String>,
47
+ pub group: Vec<String>,
48
+ }
49
+
50
+ impl Alpha {
51
+ pub fn new(assignments: Vec<Assignment>, signal: Expression) -> Self {
52
+ Alpha {
53
+ assignments,
54
+ signal,
55
+ }
56
+ }
57
+
58
+ pub fn signal(&self) -> &Expression {
59
+ &self.signal
60
+ }
61
+
62
+ pub fn assignments(&self) -> &[Assignment] {
63
+ &self.assignments
64
+ }
65
+
66
+ pub fn fields(&self) -> AlphaFields {
67
+ let defined: HashSet<_> = self
68
+ .assignments
69
+ .iter()
70
+ .map(|assignment| assignment.variable.clone())
71
+ .collect();
72
+ let mut fields = AlphaFields::default();
73
+
74
+ for assignment in &self.assignments {
75
+ collect_fields(
76
+ &assignment.value,
77
+ &Field::Matrix,
78
+ &defined,
79
+ &mut fields,
80
+ &mut Vec::new(),
81
+ );
82
+ }
83
+ collect_fields(
84
+ &self.signal,
85
+ &Field::Matrix,
86
+ &defined,
87
+ &mut fields,
88
+ &mut Vec::new(),
89
+ );
90
+ fields
91
+ }
92
+
93
+ pub fn operators(&self) -> Vec<String> {
94
+ let mut operators = Vec::new();
95
+ for assignment in &self.assignments {
96
+ collect_operators(&assignment.value, &mut operators);
97
+ }
98
+ collect_operators(&self.signal, &mut operators);
99
+ operators
100
+ }
101
+ }
102
+
103
+ fn collect_fields(
104
+ expression: &Expression,
105
+ expected: &Field,
106
+ defined: &HashSet<String>,
107
+ fields: &mut AlphaFields,
108
+ operators: &mut Vec<String>,
109
+ ) {
110
+ match expression {
111
+ Expression::Variable(name) if !defined.contains(name) => {
112
+ add_field(fields, expected, name.clone());
113
+ }
114
+ Expression::Variable(_) => {}
115
+ Expression::Placeholder(name) => add_field(fields, expected, name.clone()),
116
+ Expression::Group(expression) => {
117
+ collect_fields(expression, expected, defined, fields, operators)
118
+ }
119
+ Expression::Operation {
120
+ op,
121
+ pos_args,
122
+ kw_args: _,
123
+ } => {
124
+ add_operator(operators, op);
125
+ if let Some(operator) = get_operator(op) {
126
+ for (index, argument) in pos_args.iter().enumerate() {
127
+ if let Some(argument_type) = if operator.nary == -1 {
128
+ operator.pos_args.first()
129
+ } else {
130
+ operator.pos_args.get(index)
131
+ } {
132
+ collect_fields(argument, argument_type, defined, fields, operators);
133
+ } else {
134
+ collect_fields(argument, &Field::Matrix, defined, fields, operators);
135
+ }
136
+ }
137
+ } else {
138
+ for argument in pos_args {
139
+ collect_fields(argument, &Field::Matrix, defined, fields, operators);
140
+ }
141
+ }
142
+ }
143
+ Expression::Ternary {
144
+ cond,
145
+ then_branch,
146
+ else_branch,
147
+ } => {
148
+ collect_fields(cond, &Field::Matrix, defined, fields, operators);
149
+ collect_fields(then_branch, &Field::Matrix, defined, fields, operators);
150
+ collect_fields(else_branch, &Field::Matrix, defined, fields, operators);
151
+ }
152
+ Expression::Binary { left, right, .. } => {
153
+ collect_fields(left, &Field::Matrix, defined, fields, operators);
154
+ collect_fields(right, &Field::Matrix, defined, fields, operators);
155
+ }
156
+ Expression::Unary { expr, .. } => {
157
+ collect_fields(expr, &Field::Matrix, defined, fields, operators)
158
+ }
159
+ Expression::Constant(_) => {}
160
+ }
161
+ }
162
+
163
+ fn collect_operators(expression: &Expression, operators: &mut Vec<String>) {
164
+ match expression {
165
+ Expression::Operation { op, pos_args, .. } => {
166
+ add_operator(operators, op);
167
+ for argument in pos_args {
168
+ collect_operators(argument, operators);
169
+ }
170
+ }
171
+ Expression::Group(expression) => collect_operators(expression, operators),
172
+ Expression::Ternary {
173
+ cond,
174
+ then_branch,
175
+ else_branch,
176
+ } => {
177
+ collect_operators(cond, operators);
178
+ collect_operators(then_branch, operators);
179
+ collect_operators(else_branch, operators);
180
+ }
181
+ Expression::Binary { left, right, .. } => {
182
+ collect_operators(left, operators);
183
+ collect_operators(right, operators);
184
+ }
185
+ Expression::Unary { expr, .. } => collect_operators(expr, operators),
186
+ Expression::Constant(_) | Expression::Placeholder(_) | Expression::Variable(_) => {}
187
+ }
188
+ }
189
+
190
+ fn add_field(fields: &mut AlphaFields, expected: &Field, name: String) {
191
+ let target = match expected {
192
+ Field::Vector => &mut fields.vector,
193
+ Field::Group => &mut fields.group,
194
+ _ => &mut fields.matrix,
195
+ };
196
+ if !target.contains(&name) {
197
+ target.push(name);
198
+ }
199
+ }
200
+
201
+ fn add_operator(operators: &mut Vec<String>, name: &str) {
202
+ if !operators.iter().any(|operator| operator == name) {
203
+ operators.push(name.to_string());
204
+ }
205
+ }
206
+
207
+ #[derive(Debug, Clone)]
208
+ pub struct Assignment {
209
+ variable: String,
210
+ value: Expression,
211
+ }
212
+
213
+ impl Assignment {
214
+ pub fn new(variable: String, value: Expression) -> Self {
215
+ Assignment { variable, value }
216
+ }
217
+
218
+ pub fn variable(&self) -> &str {
219
+ &self.variable
220
+ }
221
+
222
+ pub fn value(&self) -> &Expression {
223
+ &self.value
224
+ }
225
+ }
226
+
227
+ #[derive(Debug, Clone)]
228
+ pub enum BinaryOp {
229
+ NotEqual, // !=
230
+ Equal, // ==
231
+ GreaterEqual, // >=
232
+ LessEqual, // <=
233
+ Greater, // >
234
+ Less, // <
235
+ Add, // +
236
+ Subtract, // -
237
+ Multiply, // *
238
+ Divide, // /
239
+ Or, // ||
240
+ And, // &&
241
+ }
242
+
243
+ #[derive(Debug, Clone)]
244
+ pub enum UnaryOp {
245
+ Positive, // +
246
+ Negative, // -
247
+ Not, // !
248
+ }
249
+
250
+ #[derive(Debug, Clone)]
251
+ pub enum Expression {
252
+ Ternary {
253
+ // cond ? true_val : false_val
254
+ cond: Box<Expression>,
255
+ then_branch: Box<Expression>,
256
+ else_branch: Box<Expression>,
257
+ },
258
+ Binary {
259
+ // left op right
260
+ left: Box<Expression>,
261
+ op: BinaryOp,
262
+ right: Box<Expression>,
263
+ },
264
+ Unary {
265
+ // op expr
266
+ op: UnaryOp,
267
+ expr: Box<Expression>,
268
+ },
269
+ Operation {
270
+ // op(args)
271
+ op: String,
272
+ pos_args: Vec<Expression>,
273
+ kw_args: HashMap<String, Constant>,
274
+ },
275
+ Constant(Constant),
276
+ Placeholder(String),
277
+ Variable(String),
278
+ Group(Box<Expression>),
279
+ }
@@ -1220,7 +1220,7 @@ define_operator_registry! {
1220
1220
  {
1221
1221
  "lower" => (Number, None),
1222
1222
  "upper" => (Number, None),
1223
- "inverse" => (Boolean, None),
1223
+ "inverse" => (Boolean, Some(Constant::Boolean(false))),
1224
1224
  "mask" => (Mask, Some(Constant::NaN)),
1225
1225
  },
1226
1226
  1,
@@ -109,15 +109,25 @@ fn build_alpha(pair: Pair<Rule>) -> Result<Alpha, PestError<Rule>> {
109
109
  }
110
110
  }
111
111
  }
112
- Ok(Alpha::new(
113
- assignments,
114
- signal.ok_or(PestError::new_from_span(
112
+ let signal = signal.ok_or(PestError::new_from_span(
113
+ CustomError {
114
+ message: "因子只有赋值语句, 缺少信号表达式".to_string(),
115
+ },
116
+ span,
117
+ ))?;
118
+ if let Some(signal_type) = infer_field(&signal)
119
+ && !Field::Matrix.fit(&signal_type)
120
+ {
121
+ return Err(PestError::new_from_span(
115
122
  CustomError {
116
- message: "因子只有赋值语句, 缺少信号表达式".to_string(),
123
+ message: format!(
124
+ "signal must be Matrix-compatible, but got {signal_type:?}"
125
+ ),
117
126
  },
118
127
  span,
119
- ))?,
120
- ))
128
+ ));
129
+ }
130
+ Ok(Alpha::new(assignments, signal))
121
131
  }
122
132
  other => Err(PestError::new_from_span(
123
133
  CustomError {
@@ -436,6 +446,15 @@ mod tests {
436
446
  }
437
447
  }
438
448
 
449
+ #[test]
450
+ fn signal_must_be_matrix_compatible() {
451
+ assert!(parse("group_rank(x, industry)").is_ok());
452
+ assert!(parse("vec_avg(<prices/>)").is_ok());
453
+ assert!(parse("42").is_ok());
454
+ assert!(parse("bucket(rank(x), range='0, 1, 0.1')").is_err());
455
+ assert!(parse("bucket(rank(x), buckets='0, 1')").is_err());
456
+ }
457
+
439
458
  #[test]
440
459
  fn parse_failure() {
441
460
  let inputs = [
@@ -464,6 +483,21 @@ mod tests {
464
483
  assert!(parse("divide(x)").is_err());
465
484
  }
466
485
 
486
+ #[test]
487
+ fn collects_typed_fields_and_operator_names() {
488
+ let alpha = parse("a=ts_delay(close, 5);group_rank(a, industry)").unwrap();
489
+ let fields = alpha.fields();
490
+ assert_eq!(fields.matrix, vec!["close"]);
491
+ assert_eq!(fields.group, vec!["industry"]);
492
+ assert!(fields.vector.is_empty());
493
+ assert_eq!(alpha.operators(), vec!["ts_delay", "group_rank"]);
494
+
495
+ let alpha = parse("vec_avg(<prices/>)").unwrap();
496
+ let fields = alpha.fields();
497
+ assert_eq!(fields.vector, vec!["<prices/>"]);
498
+ assert!(fields.matrix.is_empty());
499
+ }
500
+
467
501
  #[test]
468
502
  fn reports_missing_closing_parenthesis() {
469
503
  let error = parse("multiply(rank(abs(zscore(<score/>)))").unwrap_err();
@@ -275,7 +275,16 @@ py_getters!(
275
275
  },
276
276
  signal: PyResult<Py<PyExpression>>, |self, py| {
277
277
  build_pyexpression(py, self.0.signal().clone())
278
- }
278
+ },
279
+ fields: PyResult<Py<PyDict>>, |self, py| {
280
+ let fields = self.0.fields();
281
+ let dict = PyDict::new(py);
282
+ dict.set_item("matrix", fields.matrix)?;
283
+ dict.set_item("vector", fields.vector)?;
284
+ dict.set_item("group", fields.group)?;
285
+ Ok(dict.unbind())
286
+ },
287
+ operators: Vec<String>, |self, _py| { self.0.operators() },
279
288
  }
280
289
  ),
281
290
  (
@@ -1,32 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: py-fastplus
3
- Version: 0.2.1
4
- Classifier: Programming Language :: Rust
5
- Classifier: Programming Language :: Python :: Implementation :: CPython
6
- License-File: LICENSE
7
- Summary: Add your description here
8
- Author-email: AshSwing <ashswing@email.cn>
9
- Requires-Python: >=3.12
10
- Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
11
-
12
- # FastPlus - Alpha Expression Language, based on WorldQuant's Fast Expression
13
-
14
- 基于 WorldQuant Fast Expression 开发的因子表达式语法解析器.
15
-
16
- ## 平台规则
17
-
18
- - 关键字参数全部都可以使用字符串类型
19
- - 数值标量会自动广播, 可以视作 MATRIX
20
-
21
- ## 当前操作符支持范围
22
-
23
- 当前实现面向 RegularAlpha,仅收录 RegularAlpha 可用的 108 个操作符。原始操作符资料中属于 SuperAlpha 的操作符暂不支持,包括 Combo、Reduce、Special 等类别中的专用操作符。
24
-
25
- 当前与平台行为保持一致的限制:
26
-
27
- - `subtract` 目前只支持 `nary = 2`,即两个位置参数;平台的无限参数版本尚未实现。
28
- - `add`、`multiply`、`max`、`min` 等 `nary = -1` 操作符只在签名中声明一个可变参数类型,实际使用时至少需要两个位置参数,由 op-check 负责校验。
29
- - `jump_decay` 的位置参数为 `x` 和 `d`,关键字参数为 `stddev=false`、`sensitivity=0.5`、`force=0.1`。
30
- - `ts_rank_gmean_amean_diff` 要求使用 `lookback` 关键字参数,以保持表达式语法的完整性;虽然平台允许将 `d` 作为位置参数传入,FastPlus 当前不采用这种写法。
31
- - 除 `ts_rank_gmean_amean_diff` 外,包含 lookback 语义的时间序列操作符将 lookback 作为最后一个位置参数,例如 `ts_backfill(x, d)`、`ts_rank(x, d)`。
32
-
@@ -1,20 +0,0 @@
1
- # FastPlus - Alpha Expression Language, based on WorldQuant's Fast Expression
2
-
3
- 基于 WorldQuant Fast Expression 开发的因子表达式语法解析器.
4
-
5
- ## 平台规则
6
-
7
- - 关键字参数全部都可以使用字符串类型
8
- - 数值标量会自动广播, 可以视作 MATRIX
9
-
10
- ## 当前操作符支持范围
11
-
12
- 当前实现面向 RegularAlpha,仅收录 RegularAlpha 可用的 108 个操作符。原始操作符资料中属于 SuperAlpha 的操作符暂不支持,包括 Combo、Reduce、Special 等类别中的专用操作符。
13
-
14
- 当前与平台行为保持一致的限制:
15
-
16
- - `subtract` 目前只支持 `nary = 2`,即两个位置参数;平台的无限参数版本尚未实现。
17
- - `add`、`multiply`、`max`、`min` 等 `nary = -1` 操作符只在签名中声明一个可变参数类型,实际使用时至少需要两个位置参数,由 op-check 负责校验。
18
- - `jump_decay` 的位置参数为 `x` 和 `d`,关键字参数为 `stddev=false`、`sensitivity=0.5`、`force=0.1`。
19
- - `ts_rank_gmean_amean_diff` 要求使用 `lookback` 关键字参数,以保持表达式语法的完整性;虽然平台允许将 `d` 作为位置参数传入,FastPlus 当前不采用这种写法。
20
- - 除 `ts_rank_gmean_amean_diff` 外,包含 lookback 语义的时间序列操作符将 lookback 作为最后一个位置参数,例如 `ts_backfill(x, d)`、`ts_rank(x, d)`。
@@ -1,129 +0,0 @@
1
- //! AST
2
- //! 唯一结果对象: Alpha
3
- //! |-> [Assignment;] 赋值语句
4
- //! |-> [Assignment;] 赋值语句
5
- //! |-> [...;] ...
6
- //! |-> signal(Expression) 信号表达式
7
- //! Alpha 由 N 个赋值语句和 1 个信号表达式组成
8
- //!
9
- //! Assignment 赋值语句
10
- //! |-> variable(String) = value(Expression);
11
- //!
12
- //! 核心结构: Expression 表达式
13
- //! |-> Group(Expression) / Ternary / Binary / Unary 递归结构
14
- //! |-> Variable / Placeholder 占位结构
15
- //! |-> Operation* 操作符*
16
- //! |-> Constant* 常数项*
17
- //!
18
- //! 特殊规则:
19
- //! - 不可以给常数赋值 ❌ -> 刚试了下又可以了(2026-07-28)
20
- //! - 不可复用数据字段名 -> 非左值 Variable 都是数据字段
21
- //! - 参数支持字符串/字面值 -> 自动转为字符串解析
22
- //! - 支持 NaN, 不支持 Inf
23
-
24
- use std::collections::HashMap;
25
-
26
- use crate::field::Constant;
27
-
28
- use pest_derive::Parser;
29
-
30
- #[derive(Parser)]
31
- #[grammar = "fastplus.pest"]
32
- pub struct FastPlusParser;
33
-
34
- #[derive(Debug, Clone)]
35
- pub struct Alpha {
36
- assignments: Vec<Assignment>,
37
- signal: Expression,
38
- }
39
-
40
- impl Alpha {
41
- pub fn new(assignments: Vec<Assignment>, signal: Expression) -> Self {
42
- Alpha {
43
- assignments,
44
- signal,
45
- }
46
- }
47
-
48
- pub fn signal(&self) -> &Expression {
49
- &self.signal
50
- }
51
-
52
- pub fn assignments(&self) -> &[Assignment] {
53
- &self.assignments
54
- }
55
- }
56
-
57
- #[derive(Debug, Clone)]
58
- pub struct Assignment {
59
- variable: String,
60
- value: Expression,
61
- }
62
-
63
- impl Assignment {
64
- pub fn new(variable: String, value: Expression) -> Self {
65
- Assignment { variable, value }
66
- }
67
-
68
- pub fn variable(&self) -> &str {
69
- &self.variable
70
- }
71
-
72
- pub fn value(&self) -> &Expression {
73
- &self.value
74
- }
75
- }
76
-
77
- #[derive(Debug, Clone)]
78
- pub enum BinaryOp {
79
- NotEqual, // !=
80
- Equal, // ==
81
- GreaterEqual, // >=
82
- LessEqual, // <=
83
- Greater, // >
84
- Less, // <
85
- Add, // +
86
- Subtract, // -
87
- Multiply, // *
88
- Divide, // /
89
- Or, // ||
90
- And, // &&
91
- }
92
-
93
- #[derive(Debug, Clone)]
94
- pub enum UnaryOp {
95
- Positive, // +
96
- Negative, // -
97
- Not, // !
98
- }
99
-
100
- #[derive(Debug, Clone)]
101
- pub enum Expression {
102
- Ternary {
103
- // cond ? true_val : false_val
104
- cond: Box<Expression>,
105
- then_branch: Box<Expression>,
106
- else_branch: Box<Expression>,
107
- },
108
- Binary {
109
- // left op right
110
- left: Box<Expression>,
111
- op: BinaryOp,
112
- right: Box<Expression>,
113
- },
114
- Unary {
115
- // op expr
116
- op: UnaryOp,
117
- expr: Box<Expression>,
118
- },
119
- Operation {
120
- // op(args)
121
- op: String,
122
- pos_args: Vec<Expression>,
123
- kw_args: HashMap<String, Constant>,
124
- },
125
- Constant(Constant),
126
- Placeholder(String),
127
- Variable(String),
128
- Group(Box<Expression>),
129
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes